Merge pull request #31 from mokoconsulting-tech/copilot/improve-joomla-development-workflow
Improve Joomla development workflows and convert scripts to Python
This commit was merged in pull request #31.
This commit is contained in:
49
.github/workflows/joomla_testing.yml
vendored
49
.github/workflows/joomla_testing.yml
vendored
@@ -56,23 +56,46 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
|
||||||
|
- name: Cache Joomla Downloads
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/joomla-cache
|
||||||
|
key: joomla-${{ matrix.joomla-version }}
|
||||||
|
restore-keys: |
|
||||||
|
joomla-${{ matrix.joomla-version }}
|
||||||
|
|
||||||
- name: Download Joomla ${{ matrix.joomla-version }}
|
- name: Download Joomla ${{ matrix.joomla-version }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/joomla
|
mkdir -p /tmp/joomla
|
||||||
cd /tmp/joomla
|
mkdir -p /tmp/joomla-cache
|
||||||
|
|
||||||
# Download appropriate Joomla version
|
# Define ZIP file based on version
|
||||||
if [ "${{ matrix.joomla-version }}" = "4.4" ]; then
|
if [ "${{ matrix.joomla-version }}" = "4.4" ]; then
|
||||||
wget -q https://downloads.joomla.org/cms/joomla4/4-4-9/Joomla_4-4-9-Stable-Full_Package.zip
|
ZIP_FILE="Joomla_4-4-9-Stable-Full_Package.zip"
|
||||||
unzip -q Joomla_4-4-9-Stable-Full_Package.zip
|
ZIP_URL="https://downloads.joomla.org/cms/joomla4/4-4-9/${ZIP_FILE}"
|
||||||
elif [ "${{ matrix.joomla-version }}" = "5.0" ]; then
|
elif [ "${{ matrix.joomla-version }}" = "5.0" ]; then
|
||||||
wget -q https://downloads.joomla.org/cms/joomla5/5-0-3/Joomla_5-0-3-Stable-Full_Package.zip
|
ZIP_FILE="Joomla_5-0-3-Stable-Full_Package.zip"
|
||||||
unzip -q Joomla_5-0-3-Stable-Full_Package.zip
|
ZIP_URL="https://downloads.joomla.org/cms/joomla5/5-0-3/${ZIP_FILE}"
|
||||||
else
|
else
|
||||||
wget -q https://downloads.joomla.org/cms/joomla5/5-1-4/Joomla_5-1-4-Stable-Full_Package.zip
|
ZIP_FILE="Joomla_5-1-4-Stable-Full_Package.zip"
|
||||||
unzip -q Joomla_5-1-4-Stable-Full_Package.zip
|
ZIP_URL="https://downloads.joomla.org/cms/joomla5/5-1-4/${ZIP_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Use cached ZIP if available, otherwise download
|
||||||
|
if [ -f "/tmp/joomla-cache/${ZIP_FILE}" ]; then
|
||||||
|
echo "Using cached Joomla package: ${ZIP_FILE}"
|
||||||
|
cp "/tmp/joomla-cache/${ZIP_FILE}" "/tmp/joomla/"
|
||||||
|
else
|
||||||
|
echo "Downloading Joomla package: ${ZIP_FILE}"
|
||||||
|
wget -q "${ZIP_URL}" -O "/tmp/joomla/${ZIP_FILE}"
|
||||||
|
cp "/tmp/joomla/${ZIP_FILE}" "/tmp/joomla-cache/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd /tmp/joomla
|
||||||
|
unzip -q "${ZIP_FILE}"
|
||||||
|
|
||||||
- name: Configure Joomla
|
- name: Configure Joomla
|
||||||
run: |
|
run: |
|
||||||
@@ -211,6 +234,16 @@ jobs:
|
|||||||
coverage: xdebug
|
coverage: xdebug
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.composer
|
||||||
|
key: ${{ runner.os }}-composer-codeception-8.1-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-codeception-8.1-
|
||||||
|
${{ runner.os }}-composer-codeception-
|
||||||
|
${{ runner.os }}-composer-
|
||||||
|
|
||||||
- name: Install Codeception
|
- name: Install Codeception
|
||||||
run: |
|
run: |
|
||||||
composer global require codeception/codeception
|
composer global require codeception/codeception
|
||||||
|
|||||||
30
.github/workflows/php_quality.yml
vendored
30
.github/workflows/php_quality.yml
vendored
@@ -38,6 +38,16 @@ jobs:
|
|||||||
coverage: none
|
coverage: none
|
||||||
tools: cs2pr
|
tools: cs2pr
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.composer
|
||||||
|
key: ${{ runner.os }}-composer-phpcs-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-phpcs-${{ matrix.php-version }}-
|
||||||
|
${{ runner.os }}-composer-phpcs-
|
||||||
|
${{ runner.os }}-composer-
|
||||||
|
|
||||||
- name: Install PHP_CodeSniffer
|
- name: Install PHP_CodeSniffer
|
||||||
run: |
|
run: |
|
||||||
composer global require squizlabs/php_codesniffer
|
composer global require squizlabs/php_codesniffer
|
||||||
@@ -82,6 +92,16 @@ jobs:
|
|||||||
extensions: mbstring, xml, ctype, json, zip
|
extensions: mbstring, xml, ctype, json, zip
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.composer
|
||||||
|
key: ${{ runner.os }}-composer-phpstan-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-phpstan-${{ matrix.php-version }}-
|
||||||
|
${{ runner.os }}-composer-phpstan-
|
||||||
|
${{ runner.os }}-composer-
|
||||||
|
|
||||||
- name: Install PHPStan
|
- name: Install PHPStan
|
||||||
run: |
|
run: |
|
||||||
composer global require phpstan/phpstan
|
composer global require phpstan/phpstan
|
||||||
@@ -119,6 +139,16 @@ jobs:
|
|||||||
extensions: mbstring, xml, ctype, json, zip
|
extensions: mbstring, xml, ctype, json, zip
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.composer
|
||||||
|
key: ${{ runner.os }}-composer-phpcompat-8.3-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-phpcompat-8.3-
|
||||||
|
${{ runner.os }}-composer-phpcompat-
|
||||||
|
${{ runner.os }}-composer-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
composer global require squizlabs/php_codesniffer
|
composer global require squizlabs/php_codesniffer
|
||||||
|
|||||||
2
.github/workflows/repo_health.yml
vendored
2
.github/workflows/repo_health.yml
vendored
@@ -36,7 +36,7 @@ env:
|
|||||||
# Scripts governance policy
|
# Scripts governance policy
|
||||||
# Note: directories listed without a trailing slash.
|
# Note: directories listed without a trailing slash.
|
||||||
SCRIPTS_REQUIRED_DIRS:
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/git,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
# Repo health policy
|
# Repo health policy
|
||||||
# Files are listed as-is; directories must end with a trailing slash.
|
# Files are listed as-is; directories must end with a trailing slash.
|
||||||
|
|||||||
36
.gitignore
vendored
36
.gitignore
vendored
@@ -33,7 +33,11 @@ System Volume Information/
|
|||||||
*.lnk
|
*.lnk
|
||||||
Icon?
|
Icon?
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
# .vscode/ - Allow specific VS Code config files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/settings.json.example
|
||||||
|
!.vscode/extensions.json
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
@@ -789,6 +793,36 @@ package-lock.json
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
codeception.phar
|
codeception.phar
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Keep-empty folders helper
|
# Keep-empty folders helper
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
171
.vscode/tasks.json
vendored
Normal file
171
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Validate All",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/run/validate_all.sh",
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Validate Required",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make validate-required",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Run Tests",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "codecept run",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "PHP CodeSniffer",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "phpcs --standard=phpcs.xml src/",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "php",
|
||||||
|
"fileLocation": "relative",
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "^(.+):(\\d+):(\\d+):\\s+(error|warning)\\s+-\\s+(.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"message": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "PHP CodeSniffer - Auto Fix",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "phpcbf --standard=phpcs.xml src/",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "PHPStan",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "phpstan analyse --configuration=phpstan.neon",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "php",
|
||||||
|
"fileLocation": "relative",
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "^(.+):(\\d+):\\s+(.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"message": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Create Package",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/release/package_extension.sh dist",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Smoke Test",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/run/smoke_test.sh",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Script Health Check",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/run/script_health.sh",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Check Code Quality",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make quality",
|
||||||
|
"group": "test",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Fix Permissions",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make fix-permissions",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Clean",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make clean",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Install Git Hooks",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/git/install-hooks.sh",
|
||||||
|
"group": "none",
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -48,6 +48,37 @@ Contributors are expected to:
|
|||||||
* Have a working understanding of Joomla template structure.
|
* Have a working understanding of Joomla template structure.
|
||||||
* Be familiar with Git and GitHub pull request workflows.
|
* Be familiar with Git and GitHub pull request workflows.
|
||||||
* Review repository governance documents prior to submitting changes.
|
* Review repository governance documents prior to submitting changes.
|
||||||
|
* Set up the development environment using the provided tools.
|
||||||
|
|
||||||
|
### Quick Setup
|
||||||
|
|
||||||
|
For first-time contributors:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/mokoconsulting-tech/moko-cassiopeia.git
|
||||||
|
cd moko-cassiopeia
|
||||||
|
|
||||||
|
# Run development setup
|
||||||
|
make dev-setup
|
||||||
|
|
||||||
|
# Install Git hooks (optional but recommended)
|
||||||
|
./scripts/git/install-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
See [docs/QUICK_START.md](./docs/QUICK_START.md) for detailed setup instructions.
|
||||||
|
|
||||||
|
## Development Tools
|
||||||
|
|
||||||
|
The repository provides several tools to streamline development:
|
||||||
|
|
||||||
|
* **Makefile**: Common development tasks (`make help` to see all commands)
|
||||||
|
* **Pre-commit Hooks**: Automatic local validation before commits
|
||||||
|
* **VS Code Tasks**: Pre-configured tasks for common operations
|
||||||
|
* **Validation Scripts**: Located in `scripts/validate/`
|
||||||
|
* **CI/CD Workflows**: Automated testing and deployment
|
||||||
|
|
||||||
|
Run `make validate-required` before submitting PRs to catch common issues early.
|
||||||
|
|
||||||
## Contribution Workflow
|
## Contribution Workflow
|
||||||
|
|
||||||
|
|||||||
227
Makefile
Normal file
227
Makefile
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# Makefile for Moko Cassiopeia Development
|
||||||
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
.PHONY: help install validate test quality package clean dev-setup
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
# Version detection
|
||||||
|
VERSION := $(shell grep -oP '<version>\K[^<]+' src/templates/templateDetails.xml 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
## help: Show this help message
|
||||||
|
help:
|
||||||
|
@echo "Moko Cassiopeia Development Makefile"
|
||||||
|
@echo ""
|
||||||
|
@echo "Version: $(VERSION)"
|
||||||
|
@echo ""
|
||||||
|
@echo "Available targets:"
|
||||||
|
@sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /'
|
||||||
|
|
||||||
|
## install: Install development dependencies
|
||||||
|
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
|
||||||
|
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
|
||||||
|
@echo "✓ Dependencies installed"
|
||||||
|
|
||||||
|
## validate: Run all validation scripts
|
||||||
|
validate:
|
||||||
|
@echo "Running validation scripts..."
|
||||||
|
@./scripts/run/validate_all.sh
|
||||||
|
|
||||||
|
## validate-required: Run only required validation scripts
|
||||||
|
validate-required:
|
||||||
|
@echo "Running required validations..."
|
||||||
|
@./scripts/validate/manifest.sh
|
||||||
|
@./scripts/validate/xml_wellformed.sh
|
||||||
|
@./scripts/validate/workflows.sh
|
||||||
|
@echo "✓ Required validations passed"
|
||||||
|
|
||||||
|
## test: Run all tests
|
||||||
|
test:
|
||||||
|
@echo "Running tests..."
|
||||||
|
@command -v codecept >/dev/null 2>&1 || { echo "Error: codecept not found. Run 'make install' first."; exit 1; }
|
||||||
|
codecept run
|
||||||
|
|
||||||
|
## test-unit: Run unit tests only
|
||||||
|
test-unit:
|
||||||
|
@echo "Running unit tests..."
|
||||||
|
@command -v codecept >/dev/null 2>&1 || { echo "Error: codecept not found. Run 'make install' first."; exit 1; }
|
||||||
|
codecept run unit
|
||||||
|
|
||||||
|
## test-acceptance: Run acceptance tests only
|
||||||
|
test-acceptance:
|
||||||
|
@echo "Running acceptance tests..."
|
||||||
|
@command -v codecept >/dev/null 2>&1 || { echo "Error: codecept not found. Run 'make install' first."; exit 1; }
|
||||||
|
codecept run acceptance
|
||||||
|
|
||||||
|
## quality: Run code quality checks
|
||||||
|
quality:
|
||||||
|
@echo "Running code quality checks..."
|
||||||
|
@$(MAKE) phpcs
|
||||||
|
@$(MAKE) phpstan
|
||||||
|
@$(MAKE) phpcompat
|
||||||
|
|
||||||
|
## phpcs: Run PHP_CodeSniffer
|
||||||
|
phpcs:
|
||||||
|
@echo "Running PHP_CodeSniffer..."
|
||||||
|
@command -v phpcs >/dev/null 2>&1 || { echo "Error: phpcs not found. Run 'make install' first."; exit 1; }
|
||||||
|
phpcs --standard=phpcs.xml src/
|
||||||
|
|
||||||
|
## phpcs-fix: Auto-fix PHPCS violations
|
||||||
|
phpcs-fix:
|
||||||
|
@echo "Auto-fixing PHPCS violations..."
|
||||||
|
@command -v phpcbf >/dev/null 2>&1 || { echo "Error: phpcbf not found. Run 'make install' first."; exit 1; }
|
||||||
|
phpcbf --standard=phpcs.xml src/
|
||||||
|
|
||||||
|
## phpstan: Run PHPStan static analysis
|
||||||
|
phpstan:
|
||||||
|
@echo "Running PHPStan..."
|
||||||
|
@command -v phpstan >/dev/null 2>&1 || { echo "Error: phpstan not found. Run 'make install' first."; exit 1; }
|
||||||
|
phpstan analyse --configuration=phpstan.neon
|
||||||
|
|
||||||
|
## phpcompat: Check PHP 8.0+ compatibility
|
||||||
|
phpcompat:
|
||||||
|
@echo "Checking PHP 8.0+ compatibility..."
|
||||||
|
@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/
|
||||||
|
|
||||||
|
## package: Create distribution package
|
||||||
|
package:
|
||||||
|
@echo "Creating distribution package..."
|
||||||
|
@./scripts/release/package_extension.sh dist $(VERSION)
|
||||||
|
@echo "✓ Package created: dist/moko-cassiopeia-$(VERSION)-*.zip"
|
||||||
|
|
||||||
|
## smoke-test: Run smoke tests
|
||||||
|
smoke-test:
|
||||||
|
@echo "Running smoke tests..."
|
||||||
|
@./scripts/run/smoke_test.sh
|
||||||
|
|
||||||
|
## script-health: Check script health
|
||||||
|
script-health:
|
||||||
|
@echo "Checking script health..."
|
||||||
|
@./scripts/run/script_health.sh
|
||||||
|
|
||||||
|
## version-check: Display current version information
|
||||||
|
version-check:
|
||||||
|
@echo "Version Information:"
|
||||||
|
@echo " Manifest: $(VERSION)"
|
||||||
|
@echo " Latest CHANGELOG entry:"
|
||||||
|
@grep -m 1 "^## \[" CHANGELOG.md || echo " (not found)"
|
||||||
|
|
||||||
|
## fix-permissions: Fix script executable permissions
|
||||||
|
fix-permissions:
|
||||||
|
@echo "Fixing script permissions..."
|
||||||
|
@find scripts -type f -name "*.sh" -exec chmod +x {} \;
|
||||||
|
@echo "✓ Permissions fixed"
|
||||||
|
|
||||||
|
## clean: Remove generated files and caches
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning generated files..."
|
||||||
|
@rm -rf dist/
|
||||||
|
@rm -rf tests/_output/
|
||||||
|
@rm -rf .phpunit.cache/
|
||||||
|
@find . -type f -name "*.log" -delete
|
||||||
|
@find . -type f -name ".DS_Store" -delete
|
||||||
|
@echo "✓ Cleaned"
|
||||||
|
|
||||||
|
## dev-setup: Complete development environment setup
|
||||||
|
dev-setup:
|
||||||
|
@echo "Setting up development environment..."
|
||||||
|
@$(MAKE) install
|
||||||
|
@$(MAKE) fix-permissions
|
||||||
|
@echo ""
|
||||||
|
@echo "✓ Development environment ready!"
|
||||||
|
@echo ""
|
||||||
|
@echo "Quick start:"
|
||||||
|
@echo " make validate - Run all validations"
|
||||||
|
@echo " make test - Run tests"
|
||||||
|
@echo " make quality - Check code quality"
|
||||||
|
@echo " make package - Create distribution package"
|
||||||
|
|
||||||
|
## format: Format code (PHPCS auto-fix)
|
||||||
|
format: phpcs-fix
|
||||||
|
|
||||||
|
## check: Quick check (required validations + quality)
|
||||||
|
check:
|
||||||
|
@echo "Running quick checks..."
|
||||||
|
@$(MAKE) validate-required
|
||||||
|
@$(MAKE) quality
|
||||||
|
@echo "✓ All checks passed"
|
||||||
|
|
||||||
|
## all: Run everything (validate, test, quality, package)
|
||||||
|
all:
|
||||||
|
@echo "Running complete build pipeline..."
|
||||||
|
@$(MAKE) validate
|
||||||
|
@$(MAKE) test
|
||||||
|
@$(MAKE) quality
|
||||||
|
@$(MAKE) package
|
||||||
|
@echo "✓ Complete pipeline successful"
|
||||||
|
|
||||||
|
## watch: Watch for changes and run validations (requires entr)
|
||||||
|
watch:
|
||||||
|
@echo "Watching for changes... (Ctrl+C to stop)"
|
||||||
|
@command -v entr >/dev/null 2>&1 || { echo "Error: entr not found. Install with: apt-get install entr"; exit 1; }
|
||||||
|
@find src -type f | entr -c make validate-required
|
||||||
|
|
||||||
|
## list-scripts: List all available scripts
|
||||||
|
list-scripts:
|
||||||
|
@echo "Available validation scripts:"
|
||||||
|
@find scripts/validate -type f -name "*.sh" -exec basename {} \; | sort
|
||||||
|
@echo ""
|
||||||
|
@echo "Available fix scripts:"
|
||||||
|
@find scripts/fix -type f -name "*.sh" -exec basename {} \; | sort
|
||||||
|
@echo ""
|
||||||
|
@echo "Available run scripts (bash):"
|
||||||
|
@find scripts/run -type f -name "*.sh" -exec basename {} \; | sort
|
||||||
|
@echo ""
|
||||||
|
@echo "Available run scripts (python):"
|
||||||
|
@find scripts/run -type f -name "*.py" -exec basename {} \; | sort
|
||||||
|
|
||||||
|
## scaffold: Create new Joomla extension scaffolding
|
||||||
|
scaffold:
|
||||||
|
@echo "Create new Joomla extension scaffolding"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage: make scaffold TYPE=<type> NAME=<name> AUTHOR=<author> DESC=<description>"
|
||||||
|
@echo ""
|
||||||
|
@echo "Types: component, module, plugin, template, package"
|
||||||
|
@echo ""
|
||||||
|
@echo "Example:"
|
||||||
|
@echo " make scaffold TYPE=module NAME='My Module' AUTHOR='John Doe' DESC='Module description'"
|
||||||
|
|
||||||
|
## scaffold-component: Create a component
|
||||||
|
scaffold-component:
|
||||||
|
@python3 scripts/run/scaffold_extension.py component "$(NAME)" "$(DESC)" "$(AUTHOR)"
|
||||||
|
|
||||||
|
## scaffold-module: Create a module
|
||||||
|
scaffold-module:
|
||||||
|
@python3 scripts/run/scaffold_extension.py module "$(NAME)" "$(DESC)" "$(AUTHOR)" --client $(CLIENT)
|
||||||
|
|
||||||
|
## scaffold-plugin: Create a plugin
|
||||||
|
scaffold-plugin:
|
||||||
|
@python3 scripts/run/scaffold_extension.py plugin "$(NAME)" "$(DESC)" "$(AUTHOR)" --group $(GROUP)
|
||||||
|
|
||||||
|
## scaffold-template: Create a template
|
||||||
|
scaffold-template:
|
||||||
|
@python3 scripts/run/scaffold_extension.py template "$(NAME)" "$(DESC)" "$(AUTHOR)"
|
||||||
|
|
||||||
|
## scaffold-package: Create a package
|
||||||
|
scaffold-package:
|
||||||
|
@python3 scripts/run/scaffold_extension.py package "$(NAME)" "$(DESC)" "$(AUTHOR)"
|
||||||
|
|
||||||
|
## docs: Open documentation
|
||||||
|
docs:
|
||||||
|
@echo "Documentation files:"
|
||||||
|
@echo " README.md - Project overview"
|
||||||
|
@echo " docs/JOOMLA_DEVELOPMENT.md - Development guide"
|
||||||
|
@echo " docs/WORKFLOW_GUIDE.md - Workflow reference"
|
||||||
|
@echo " scripts/README.md - Scripts documentation"
|
||||||
|
@echo " CHANGELOG.md - Version history"
|
||||||
|
@echo " CONTRIBUTING.md - Contribution guide"
|
||||||
65
README.md
65
README.md
@@ -134,6 +134,44 @@ If upgrading from a prior version, Joomla will safely overwrite files
|
|||||||
|
|
||||||
For developers and contributors working on the moko-cassiopeia template:
|
For developers and contributors working on the moko-cassiopeia template:
|
||||||
|
|
||||||
|
### Quick Start for Developers
|
||||||
|
|
||||||
|
Get started in minutes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup development environment
|
||||||
|
make dev-setup
|
||||||
|
|
||||||
|
# Validate code
|
||||||
|
make validate-required
|
||||||
|
|
||||||
|
# Check code quality
|
||||||
|
make quality
|
||||||
|
|
||||||
|
# Create distribution package
|
||||||
|
make package
|
||||||
|
|
||||||
|
# Install Git hooks (optional but recommended)
|
||||||
|
./scripts/git/install-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**New to the project?** See [Quick Start Guide](./docs/QUICK_START.md) for a 5-minute walkthrough.
|
||||||
|
|
||||||
|
### Development Resources
|
||||||
|
|
||||||
|
- **[Quick Start Guide](./docs/QUICK_START.md)** - Get up and running in 5 minutes
|
||||||
|
- **[Workflow Guide](./docs/WORKFLOW_GUIDE.md)** - Complete workflow reference with examples
|
||||||
|
- **[Joomla Development Guide](./docs/JOOMLA_DEVELOPMENT.md)** - Testing, quality checks, and deployment
|
||||||
|
- **[Scripts Documentation](./scripts/README.md)** - Available automation scripts
|
||||||
|
- **[Contributing Guide](./CONTRIBUTING.md)** - How to contribute
|
||||||
|
|
||||||
|
### Available Tools
|
||||||
|
|
||||||
|
- **Makefile**: Run `make help` to see all available commands
|
||||||
|
- **Pre-commit Hooks**: Automatic validation before commits
|
||||||
|
- **VS Code Tasks**: Pre-configured development tasks
|
||||||
|
- **GitHub Actions**: Automated CI/CD pipelines
|
||||||
|
|
||||||
### Joomla Development Workflows
|
### Joomla Development Workflows
|
||||||
|
|
||||||
Comprehensive Joomla-aware development tools and workflows are available:
|
Comprehensive Joomla-aware development tools and workflows are available:
|
||||||
@@ -141,32 +179,9 @@ Comprehensive Joomla-aware development tools and workflows are available:
|
|||||||
- **Extension Packaging** - Create distributable ZIP packages
|
- **Extension Packaging** - Create distributable ZIP packages
|
||||||
- **PHP Quality Checks** - PHPStan and PHP_CodeSniffer with Joomla standards
|
- **PHP Quality Checks** - PHPStan and PHP_CodeSniffer with Joomla standards
|
||||||
- **Automated Testing** - Codeception framework with multiple Joomla versions
|
- **Automated Testing** - Codeception framework with multiple Joomla versions
|
||||||
- **CI/CD Pipelines** - GitHub Actions for testing and deployment
|
- **CI/CD Pipelines** - GitHub Actions with caching for faster builds
|
||||||
|
|
||||||
See the [Joomla Development Guide](./docs/JOOMLA_DEVELOPMENT.md) for:
|
Run `make help` to see all available development commands.
|
||||||
- Setup instructions for local development
|
|
||||||
- Running tests and quality checks
|
|
||||||
- Creating release packages
|
|
||||||
- Deployment workflows
|
|
||||||
- CI/CD pipeline details
|
|
||||||
|
|
||||||
### Quick Start for Developers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate code
|
|
||||||
./scripts/validate/php_syntax.sh
|
|
||||||
./scripts/validate/manifest.sh
|
|
||||||
|
|
||||||
# Create distribution package
|
|
||||||
./scripts/release/package_extension.sh dist 3.5.0
|
|
||||||
|
|
||||||
# Run tests (requires Codeception)
|
|
||||||
codecept run
|
|
||||||
|
|
||||||
# Check code quality (requires PHPStan/PHPCS)
|
|
||||||
phpstan analyse --configuration=phpstan.neon
|
|
||||||
phpcs --standard=phpcs.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
|||||||
352
docs/QUICK_START.md
Normal file
352
docs/QUICK_START.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Quick Start Guide - Moko Cassiopeia Development
|
||||||
|
|
||||||
|
Get up and running with Moko Cassiopeia development in minutes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have:
|
||||||
|
|
||||||
|
- **Git** - For version control
|
||||||
|
- **PHP 8.0+** - Required runtime
|
||||||
|
- **Composer** - PHP dependency manager
|
||||||
|
- **Make** (optional) - For convenient commands
|
||||||
|
- **Code Editor** - VS Code recommended (tasks pre-configured)
|
||||||
|
|
||||||
|
## 5-Minute Setup
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/mokoconsulting-tech/moko-cassiopeia.git
|
||||||
|
cd moko-cassiopeia
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Development Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make (recommended)
|
||||||
|
make dev-setup
|
||||||
|
|
||||||
|
# Or manually
|
||||||
|
composer global require squizlabs/php_codesniffer
|
||||||
|
composer global require phpstan/phpstan
|
||||||
|
composer global require phpcompatibility/php-compatibility
|
||||||
|
composer global require codeception/codeception
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install Git Hooks (Optional but Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/git/install-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Validate Everything Works
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick validation
|
||||||
|
make validate-required
|
||||||
|
|
||||||
|
# Or comprehensive validation
|
||||||
|
make validate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Make your changes
|
||||||
|
vim src/templates/index.php
|
||||||
|
|
||||||
|
# 2. Validate locally
|
||||||
|
make validate-required
|
||||||
|
|
||||||
|
# 3. Check code quality
|
||||||
|
make quality
|
||||||
|
|
||||||
|
# 4. Commit
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat: add new feature"
|
||||||
|
# (pre-commit hook runs automatically)
|
||||||
|
|
||||||
|
# 5. Push
|
||||||
|
git push origin your-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run unit tests only
|
||||||
|
make test-unit
|
||||||
|
|
||||||
|
# Run acceptance tests only
|
||||||
|
make test-acceptance
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check everything
|
||||||
|
make quality
|
||||||
|
|
||||||
|
# PHP CodeSniffer only
|
||||||
|
make phpcs
|
||||||
|
|
||||||
|
# Auto-fix PHPCS issues
|
||||||
|
make phpcs-fix
|
||||||
|
|
||||||
|
# PHPStan only
|
||||||
|
make phpstan
|
||||||
|
|
||||||
|
# PHP compatibility check
|
||||||
|
make phpcompat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Release Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Package with auto-detected version
|
||||||
|
make package
|
||||||
|
|
||||||
|
# Or specify directory and version
|
||||||
|
./scripts/release/package_extension.sh dist 03.05.00
|
||||||
|
|
||||||
|
# Check package contents
|
||||||
|
ls -lh dist/
|
||||||
|
unzip -l dist/moko-cassiopeia-*.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## VS Code Integration
|
||||||
|
|
||||||
|
If using VS Code, press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) and type "Run Task" to see available tasks:
|
||||||
|
|
||||||
|
- **Validate All** - Run all validation scripts (default test task)
|
||||||
|
- **Validate Required** - Run only required validations
|
||||||
|
- **PHP CodeSniffer** - Check code style
|
||||||
|
- **PHP CodeSniffer - Auto Fix** - Fix code style issues
|
||||||
|
- **PHPStan** - Static analysis
|
||||||
|
- **Run Tests** - Execute all tests
|
||||||
|
- **Create Package** - Build distribution ZIP
|
||||||
|
- **Install Git Hooks** - Set up pre-commit hooks
|
||||||
|
|
||||||
|
## Available Make Commands
|
||||||
|
|
||||||
|
Run `make help` to see all available commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make help # Show all commands
|
||||||
|
make dev-setup # Complete environment setup
|
||||||
|
make validate # Run all validations
|
||||||
|
make test # Run all tests
|
||||||
|
make quality # Check code quality
|
||||||
|
make package # Create distribution package
|
||||||
|
make clean # Remove generated files
|
||||||
|
make check # Quick check (validate + quality)
|
||||||
|
make all # Complete build pipeline
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
moko-cassiopeia/
|
||||||
|
├── src/ # Joomla template source
|
||||||
|
│ ├── templates/ # Template files
|
||||||
|
│ ├── media/ # Assets (CSS, JS, images)
|
||||||
|
│ ├── language/ # Language files
|
||||||
|
│ └── administrator/ # Admin files
|
||||||
|
├── scripts/ # Automation scripts
|
||||||
|
│ ├── validate/ # Validation scripts
|
||||||
|
│ ├── fix/ # Fix/update scripts
|
||||||
|
│ ├── release/ # Release scripts
|
||||||
|
│ ├── run/ # Execution scripts
|
||||||
|
│ ├── git/ # Git hooks
|
||||||
|
│ └── lib/ # Shared libraries
|
||||||
|
├── tests/ # Test suites
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── .github/workflows/ # CI/CD workflows
|
||||||
|
├── Makefile # Make commands
|
||||||
|
└── README.md # Project overview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Learning the Workflow
|
||||||
|
|
||||||
|
1. **Read the Workflow Guide**: [docs/WORKFLOW_GUIDE.md](./WORKFLOW_GUIDE.md)
|
||||||
|
2. **Review Joomla Development**: [docs/JOOMLA_DEVELOPMENT.md](./JOOMLA_DEVELOPMENT.md)
|
||||||
|
3. **Check Scripts Documentation**: [scripts/README.md](../scripts/README.md)
|
||||||
|
|
||||||
|
### Creating Your First Feature
|
||||||
|
|
||||||
|
1. **Create a version branch** via GitHub Actions:
|
||||||
|
- Go to Actions → Create version branch
|
||||||
|
- Enter version (e.g., 03.06.00)
|
||||||
|
- Select branch prefix: `dev/`
|
||||||
|
- Run workflow
|
||||||
|
|
||||||
|
2. **Checkout the branch**:
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git checkout dev/03.06.00
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make changes and test**:
|
||||||
|
```bash
|
||||||
|
# Edit files
|
||||||
|
vim src/templates/index.php
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
make validate-required
|
||||||
|
|
||||||
|
# Check quality
|
||||||
|
make quality
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Commit and push**:
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat: your feature description"
|
||||||
|
git push origin dev/03.06.00
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Watch CI**: Check GitHub Actions for automated testing
|
||||||
|
|
||||||
|
### Understanding the Release Process
|
||||||
|
|
||||||
|
```
|
||||||
|
Development → RC → Stable → Production
|
||||||
|
(dev/) (rc/) (version/) (main)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **dev/X.Y.Z** - Active development
|
||||||
|
2. **rc/X.Y.Z** - Release candidate testing
|
||||||
|
3. **version/X.Y.Z** - Stable release
|
||||||
|
4. **main** - Production (auto-merged from version/)
|
||||||
|
|
||||||
|
Use the Release Pipeline workflow to promote between stages.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Scripts Not Executable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make fix-permissions
|
||||||
|
# Or manually:
|
||||||
|
chmod +x scripts/**/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHPStan/PHPCS Not Found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
# Or manually:
|
||||||
|
composer global require squizlabs/php_codesniffer phpstan/phpstan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-commit Hook Fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run manually to see details
|
||||||
|
./scripts/git/pre-commit.sh
|
||||||
|
|
||||||
|
# Quick mode (skip some checks)
|
||||||
|
./scripts/git/pre-commit.sh --quick
|
||||||
|
|
||||||
|
# Skip quality checks
|
||||||
|
./scripts/git/pre-commit.sh --skip-quality
|
||||||
|
|
||||||
|
# Bypass hook (not recommended)
|
||||||
|
git commit --no-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI Workflow Fails
|
||||||
|
|
||||||
|
1. Check the workflow logs in GitHub Actions
|
||||||
|
2. Run the same checks locally:
|
||||||
|
```bash
|
||||||
|
./scripts/validate/manifest.sh
|
||||||
|
./scripts/validate/php_syntax.sh
|
||||||
|
make quality
|
||||||
|
```
|
||||||
|
|
||||||
|
### Need Help?
|
||||||
|
|
||||||
|
- **Documentation**: Check [docs/](../docs/) directory
|
||||||
|
- **Issues**: Open an issue on GitHub
|
||||||
|
- **Contributing**: See [CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Before Committing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Always validate first
|
||||||
|
make validate-required
|
||||||
|
|
||||||
|
# Check quality for PHP changes
|
||||||
|
make quality
|
||||||
|
|
||||||
|
# Run tests if you changed functionality
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- Follow PSR-12 standards
|
||||||
|
- Use `make phpcs-fix` to auto-fix issues
|
||||||
|
- Add SPDX license headers to new files
|
||||||
|
- Keep functions small and focused
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Update docs when changing workflows
|
||||||
|
- Add comments for complex logic
|
||||||
|
- Update CHANGELOG.md with changes
|
||||||
|
- Keep README.md current
|
||||||
|
|
||||||
|
### Version Management
|
||||||
|
|
||||||
|
- Use semantic versioning: Major.Minor.Patch (03.05.00)
|
||||||
|
- Update CHANGELOG.md with all changes
|
||||||
|
- Follow the version hierarchy: dev → rc → version → main
|
||||||
|
- Never skip stages in the release process
|
||||||
|
|
||||||
|
## Useful Resources
|
||||||
|
|
||||||
|
- [Joomla Documentation](https://docs.joomla.org/)
|
||||||
|
- [PSR-12 Coding Standard](https://www.php-fig.org/psr/psr-12/)
|
||||||
|
- [Semantic Versioning](https://semver.org/)
|
||||||
|
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup
|
||||||
|
make dev-setup # Initial setup
|
||||||
|
./scripts/git/install-hooks.sh # Install hooks
|
||||||
|
|
||||||
|
# Development
|
||||||
|
make validate-required # Quick validation
|
||||||
|
make quality # Code quality
|
||||||
|
make test # Run tests
|
||||||
|
|
||||||
|
# Building
|
||||||
|
make package # Create ZIP
|
||||||
|
|
||||||
|
# Maintenance
|
||||||
|
make clean # Clean generated files
|
||||||
|
make fix-permissions # Fix script permissions
|
||||||
|
|
||||||
|
# Help
|
||||||
|
make help # Show all commands
|
||||||
|
./scripts/run/validate_all.sh --help # Script help
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version:** 1.0.0
|
||||||
|
**Last Updated:** 2025-01-04
|
||||||
|
**Get Started:** Run `make dev-setup` now!
|
||||||
472
docs/WORKFLOW_GUIDE.md
Normal file
472
docs/WORKFLOW_GUIDE.md
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
# Workflow Guide - Moko Cassiopeia
|
||||||
|
|
||||||
|
Quick reference guide for GitHub Actions workflows and common development tasks.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Workflow Quick Reference](#workflow-quick-reference)
|
||||||
|
- [Common Development Tasks](#common-development-tasks)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Best Practices](#best-practices)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This repository uses GitHub Actions for continuous integration, testing, quality checks, and deployment. All workflows are located in `.github/workflows/`.
|
||||||
|
|
||||||
|
### Workflow Execution Model
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Code Changes │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ CI Pipeline │ ← Validation, Testing, Quality
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Version Branch │ ← Create dev/X.Y.Z branch
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Release Pipeline│ ← dev → rc → version → main
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Distribution │ ← ZIP package + GitHub Release
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Quick Reference
|
||||||
|
|
||||||
|
### Continuous Integration (ci.yml)
|
||||||
|
|
||||||
|
**Trigger:** Automatic on push/PR to main, dev/*, rc/*, version/* branches
|
||||||
|
|
||||||
|
**Purpose:** Validates code quality and repository structure
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Validates Joomla manifest XML
|
||||||
|
- ✅ Checks XML well-formedness
|
||||||
|
- ✅ Validates GitHub Actions workflows
|
||||||
|
- ✅ Runs PHP syntax checks
|
||||||
|
- ✅ Checks for secrets in code
|
||||||
|
- ✅ Validates license headers
|
||||||
|
- ✅ Verifies version alignment
|
||||||
|
|
||||||
|
**When to check:** After every commit
|
||||||
|
|
||||||
|
**How to view results:**
|
||||||
|
```bash
|
||||||
|
# Via GitHub CLI
|
||||||
|
gh run list --workflow=ci.yml --limit 5
|
||||||
|
gh run view <run-id> --log
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP Quality Checks (php_quality.yml)
|
||||||
|
|
||||||
|
**Trigger:** Automatic on push/PR to main, dev/*, rc/*, version/* branches
|
||||||
|
|
||||||
|
**Purpose:** Ensures PHP code quality and compatibility
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- 🔍 PHPStan static analysis (level 5)
|
||||||
|
- 📏 PHP_CodeSniffer with PSR-12 standards
|
||||||
|
- ✔️ PHP 8.0+ compatibility checks
|
||||||
|
|
||||||
|
**Matrix:** PHP 8.0, 8.1, 8.2, 8.3
|
||||||
|
|
||||||
|
**When to check:** Before committing PHP changes
|
||||||
|
|
||||||
|
**How to run locally:**
|
||||||
|
```bash
|
||||||
|
# Install tools
|
||||||
|
composer global require squizlabs/php_codesniffer phpstan/phpstan
|
||||||
|
|
||||||
|
# Run checks
|
||||||
|
phpcs --standard=phpcs.xml src/
|
||||||
|
phpstan analyse --configuration=phpstan.neon
|
||||||
|
```
|
||||||
|
|
||||||
|
### Joomla Testing (joomla_testing.yml)
|
||||||
|
|
||||||
|
**Trigger:** Automatic on push/PR to main, dev/*, rc/* branches
|
||||||
|
|
||||||
|
**Purpose:** Tests template compatibility with Joomla versions
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- 📦 Downloads and installs Joomla (4.4, 5.0, 5.1)
|
||||||
|
- 🔧 Installs template into Joomla
|
||||||
|
- ✅ Validates template installation
|
||||||
|
- 🧪 Runs Codeception tests
|
||||||
|
|
||||||
|
**Matrix:** Joomla 4.4/5.0/5.1 × PHP 8.0/8.1/8.2/8.3
|
||||||
|
|
||||||
|
**When to check:** Before releasing new versions
|
||||||
|
|
||||||
|
**How to test locally:**
|
||||||
|
```bash
|
||||||
|
# See docs/JOOMLA_DEVELOPMENT.md for local testing setup
|
||||||
|
codecept run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Branch Creation (version_branch.yml)
|
||||||
|
|
||||||
|
**Trigger:** Manual workflow dispatch
|
||||||
|
|
||||||
|
**Purpose:** Creates a new version branch and bumps version numbers
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- 🏷️ Creates dev/*, rc/*, or version/* branch
|
||||||
|
- 📝 Updates version in all files
|
||||||
|
- 📅 Updates manifest dates
|
||||||
|
- 📋 Moves CHANGELOG unreleased to version
|
||||||
|
- ✅ Validates version hierarchy
|
||||||
|
|
||||||
|
**When to use:** Starting work on a new version
|
||||||
|
|
||||||
|
**How to run:**
|
||||||
|
1. Go to Actions → Create version branch
|
||||||
|
2. Click "Run workflow"
|
||||||
|
3. Enter version (e.g., 03.05.00)
|
||||||
|
4. Select branch prefix (dev/, rc/, or version/)
|
||||||
|
5. Click "Run workflow"
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```yaml
|
||||||
|
new_version: 03.06.00
|
||||||
|
branch_prefix: dev/
|
||||||
|
version_text: beta
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Pipeline (release_pipeline.yml)
|
||||||
|
|
||||||
|
**Trigger:** Manual workflow dispatch or release event
|
||||||
|
|
||||||
|
**Purpose:** Promotes branches through release stages and creates distributions
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- 🔄 Promotes branches: dev → rc → version → main
|
||||||
|
- 📅 Normalizes dates in manifest and CHANGELOG
|
||||||
|
- 📦 Builds distributable ZIP package
|
||||||
|
- 🚀 Uploads to SFTP server
|
||||||
|
- 🏷️ Creates Git tag
|
||||||
|
- 📋 Creates GitHub Release
|
||||||
|
- 🔒 Attests build provenance
|
||||||
|
|
||||||
|
**When to use:** Promoting a version through release stages
|
||||||
|
|
||||||
|
**How to run:**
|
||||||
|
1. Go to Actions → Release Pipeline
|
||||||
|
2. Click "Run workflow"
|
||||||
|
3. Select classification (auto/rc/stable)
|
||||||
|
4. Click "Run workflow"
|
||||||
|
|
||||||
|
**Release flow:**
|
||||||
|
```
|
||||||
|
dev/X.Y.Z → rc/X.Y.Z → version/X.Y.Z → main
|
||||||
|
(dev) (RC) (stable) (production)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Staging (deploy_staging.yml)
|
||||||
|
|
||||||
|
**Trigger:** Manual workflow dispatch
|
||||||
|
|
||||||
|
**Purpose:** Deploys template to staging/development environments
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Validates deployment prerequisites
|
||||||
|
- 📦 Builds deployment package
|
||||||
|
- 🚀 Uploads via SFTP to environment
|
||||||
|
- 📝 Creates deployment summary
|
||||||
|
|
||||||
|
**When to use:** Testing in staging before production release
|
||||||
|
|
||||||
|
**How to run:**
|
||||||
|
1. Go to Actions → Deploy to Staging
|
||||||
|
2. Click "Run workflow"
|
||||||
|
3. Select environment (staging/development/preview)
|
||||||
|
4. Optionally specify version
|
||||||
|
5. Click "Run workflow"
|
||||||
|
|
||||||
|
**Required secrets:**
|
||||||
|
- `STAGING_HOST` - SFTP hostname
|
||||||
|
- `STAGING_USER` - SFTP username
|
||||||
|
- `STAGING_KEY` - SSH private key (or `STAGING_PASSWORD`)
|
||||||
|
- `STAGING_PATH` - Remote deployment path
|
||||||
|
|
||||||
|
### Repository Health (repo_health.yml)
|
||||||
|
|
||||||
|
**Trigger:** Manual workflow dispatch (admin only)
|
||||||
|
|
||||||
|
**Purpose:** Comprehensive repository health and configuration checks
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- 🔐 Validates release configuration
|
||||||
|
- 🌐 Tests SFTP connectivity
|
||||||
|
- 📂 Checks scripts governance
|
||||||
|
- 📄 Validates required artifacts
|
||||||
|
- 🔍 Extended checks (SPDX, ShellCheck, etc.)
|
||||||
|
|
||||||
|
**When to use:** Before major releases or when debugging deployment issues
|
||||||
|
|
||||||
|
**How to run:**
|
||||||
|
1. Go to Actions → Repo Health
|
||||||
|
2. Click "Run workflow"
|
||||||
|
3. Select profile (all/release/scripts/repo)
|
||||||
|
4. Click "Run workflow"
|
||||||
|
|
||||||
|
**Profiles:**
|
||||||
|
- `all` - Run all checks
|
||||||
|
- `release` - Release configuration and SFTP only
|
||||||
|
- `scripts` - Scripts governance only
|
||||||
|
- `repo` - Repository health only
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Starting a New Feature
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create a new version branch via GitHub Actions
|
||||||
|
# Actions → Create version branch → dev/X.Y.Z
|
||||||
|
|
||||||
|
# 2. Clone and checkout the new branch
|
||||||
|
git fetch origin
|
||||||
|
git checkout dev/X.Y.Z
|
||||||
|
|
||||||
|
# 3. Make your changes
|
||||||
|
vim src/templates/index.php
|
||||||
|
|
||||||
|
# 4. Validate locally
|
||||||
|
./scripts/validate/php_syntax.sh
|
||||||
|
./scripts/validate/manifest.sh
|
||||||
|
|
||||||
|
# 5. Commit and push
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat: add new feature"
|
||||||
|
git push origin dev/X.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running All Validations Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run comprehensive validation suite
|
||||||
|
./scripts/run/validate_all.sh
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
./scripts/run/validate_all.sh -v
|
||||||
|
|
||||||
|
# Run smoke tests
|
||||||
|
./scripts/run/smoke_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Release Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Package with auto-detected version
|
||||||
|
./scripts/release/package_extension.sh
|
||||||
|
|
||||||
|
# Package with specific version
|
||||||
|
./scripts/release/package_extension.sh dist 03.05.00
|
||||||
|
|
||||||
|
# Verify package contents
|
||||||
|
unzip -l dist/moko-cassiopeia-*.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating Version Numbers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via GitHub Actions (recommended)
|
||||||
|
# Actions → Create version branch
|
||||||
|
|
||||||
|
# Or manually with scripts
|
||||||
|
./scripts/fix/versions.sh 03.05.00
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating CHANGELOG
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add new version entry
|
||||||
|
./scripts/release/update_changelog.sh 03.05.00
|
||||||
|
|
||||||
|
# Update release dates
|
||||||
|
./scripts/release/update_dates.sh 2025-01-15 03.05.00
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### CI Failures
|
||||||
|
|
||||||
|
#### PHP Syntax Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check specific file
|
||||||
|
php -l src/templates/index.php
|
||||||
|
|
||||||
|
# Run validation script
|
||||||
|
./scripts/validate/php_syntax.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Manifest Validation Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate manifest XML
|
||||||
|
./scripts/validate/manifest.sh
|
||||||
|
|
||||||
|
# Check XML well-formedness
|
||||||
|
./scripts/validate/xml_wellformed.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version Alignment Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check version in manifest matches CHANGELOG
|
||||||
|
./scripts/validate/version_alignment.sh
|
||||||
|
|
||||||
|
# Fix versions
|
||||||
|
./scripts/fix/versions.sh 03.05.00
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow Failures
|
||||||
|
|
||||||
|
#### "Permission denied" on scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix script permissions
|
||||||
|
chmod +x scripts/**/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Branch already exists"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check existing branches
|
||||||
|
git branch -r | grep dev/
|
||||||
|
|
||||||
|
# Delete remote branch if needed (carefully!)
|
||||||
|
git push origin --delete dev/03.05.00
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Missing required secrets"
|
||||||
|
|
||||||
|
Go to repository Settings → Secrets and variables → Actions, and add:
|
||||||
|
- `FTP_HOST`
|
||||||
|
- `FTP_USER`
|
||||||
|
- `FTP_KEY` or `FTP_PASSWORD`
|
||||||
|
- `FTP_PATH`
|
||||||
|
|
||||||
|
#### SFTP Connection Failed
|
||||||
|
|
||||||
|
1. Verify credentials in repo_health workflow:
|
||||||
|
- Actions → Repo Health → profile: release
|
||||||
|
|
||||||
|
2. Check SSH key format (OpenSSH, not PuTTY PPK)
|
||||||
|
|
||||||
|
3. Verify server allows connections from GitHub IPs
|
||||||
|
|
||||||
|
### Quality Check Failures
|
||||||
|
|
||||||
|
#### PHPStan Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run locally to see full output
|
||||||
|
phpstan analyse --configuration=phpstan.neon
|
||||||
|
|
||||||
|
# Generate baseline to ignore existing issues
|
||||||
|
phpstan analyse --configuration=phpstan.neon --generate-baseline
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PHPCS Violations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check violations
|
||||||
|
phpcs --standard=phpcs.xml src/
|
||||||
|
|
||||||
|
# Auto-fix where possible
|
||||||
|
phpcbf --standard=phpcs.xml src/
|
||||||
|
|
||||||
|
# Show specific error codes
|
||||||
|
phpcs --standard=phpcs.xml --report=source src/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Joomla Testing Failed
|
||||||
|
|
||||||
|
1. Check PHP/Joomla version matrix compatibility
|
||||||
|
2. Review MySQL connection errors
|
||||||
|
3. Verify template manifest structure
|
||||||
|
4. Check template file paths
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Version Management
|
||||||
|
|
||||||
|
1. **Always use version branches:** dev/X.Y.Z, rc/X.Y.Z, version/X.Y.Z
|
||||||
|
2. **Follow hierarchy:** dev → rc → version → main
|
||||||
|
3. **Update CHANGELOG:** Document all changes in Unreleased section
|
||||||
|
4. **Semantic versioning:** Major.Minor.Patch (03.05.00)
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
1. **Run validations locally** before pushing
|
||||||
|
2. **Fix PHPStan warnings** at level 5
|
||||||
|
3. **Follow PSR-12** coding standards
|
||||||
|
4. **Add SPDX license headers** to new files
|
||||||
|
5. **Keep functions small** and well-documented
|
||||||
|
|
||||||
|
### Workflow Usage
|
||||||
|
|
||||||
|
1. **Use CI for every commit** - automated validation
|
||||||
|
2. **Run repo_health before releases** - comprehensive checks
|
||||||
|
3. **Test on staging first** - never deploy directly to production
|
||||||
|
4. **Monitor workflow runs** - fix failures promptly
|
||||||
|
5. **Review workflow logs** - understand what changed
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
|
||||||
|
1. **Create dev branch** → Work on features
|
||||||
|
2. **Promote to rc** → Release candidate testing
|
||||||
|
3. **Promote to version** → Stable release
|
||||||
|
4. **Merge to main** → Production (auto-merged via PR)
|
||||||
|
5. **Create GitHub Release** → Public distribution
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
1. **Never commit secrets** - use GitHub Secrets
|
||||||
|
2. **Use SSH keys** for SFTP (not passwords)
|
||||||
|
3. **Scan for secrets** - runs automatically in CI
|
||||||
|
4. **Keep dependencies updated** - security patches
|
||||||
|
5. **Review security advisories** - GitHub Dependabot
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
1. **Update docs with code** - keep in sync
|
||||||
|
2. **Document workflow changes** - update this guide
|
||||||
|
3. **Add examples** - show, don't just tell
|
||||||
|
4. **Link to relevant docs** - cross-reference
|
||||||
|
5. **Keep README current** - first impression matters
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Main README](../README.md) - Project overview
|
||||||
|
- [Joomla Development Guide](./JOOMLA_DEVELOPMENT.md) - Testing and quality
|
||||||
|
- [Scripts README](../scripts/README.md) - Script documentation
|
||||||
|
- [CHANGELOG](../CHANGELOG.md) - Version history
|
||||||
|
- [CONTRIBUTING](../CONTRIBUTING.md) - Contribution guidelines
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
1. **Check workflow logs** - Most issues have clear error messages
|
||||||
|
2. **Review this guide** - Common solutions documented
|
||||||
|
3. **Run validation scripts** - Identify specific issues
|
||||||
|
4. **Open an issue** - For bugs or questions
|
||||||
|
5. **Contact maintainers** - For access or configuration issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version:** 1.0.0
|
||||||
|
**Last Updated:** 2025-01-04
|
||||||
|
**Maintained by:** Moko Consulting Engineering
|
||||||
56
scripts/git/install-hooks.sh
Executable file
56
scripts/git/install-hooks.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install Git hooks for Moko Cassiopeia
|
||||||
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Moko-Cassiopeia.Scripts
|
||||||
|
# INGROUP: Scripts.Git
|
||||||
|
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
# FILE: ./scripts/git/install-hooks.sh
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Install Git hooks for local development
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
echo "Installing Git hooks..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create .git/hooks directory if it doesn't exist
|
||||||
|
mkdir -p "${REPO_ROOT}/.git/hooks"
|
||||||
|
|
||||||
|
# Install pre-commit hook
|
||||||
|
PRE_COMMIT_HOOK="${REPO_ROOT}/.git/hooks/pre-commit"
|
||||||
|
cat > "${PRE_COMMIT_HOOK}" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pre-commit hook - installed by scripts/git/install-hooks.sh
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(git rev-parse --show-toplevel)/scripts/git"
|
||||||
|
|
||||||
|
if [ -f "${SCRIPT_DIR}/pre-commit.sh" ]; then
|
||||||
|
exec "${SCRIPT_DIR}/pre-commit.sh" "$@"
|
||||||
|
else
|
||||||
|
echo "Error: pre-commit.sh not found in ${SCRIPT_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "${PRE_COMMIT_HOOK}"
|
||||||
|
|
||||||
|
echo "✓ Installed pre-commit hook"
|
||||||
|
echo ""
|
||||||
|
echo "The pre-commit hook will run automatically before each commit."
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " - Skip hook: git commit --no-verify"
|
||||||
|
echo " - Quick mode: ./scripts/git/pre-commit.sh --quick"
|
||||||
|
echo " - Skip quality checks: ./scripts/git/pre-commit.sh --skip-quality"
|
||||||
|
echo ""
|
||||||
|
echo "To uninstall hooks:"
|
||||||
|
echo " rm .git/hooks/pre-commit"
|
||||||
|
echo ""
|
||||||
|
echo "Done!"
|
||||||
272
scripts/git/pre-commit.sh
Executable file
272
scripts/git/pre-commit.sh
Executable file
@@ -0,0 +1,272 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pre-commit hook script for Moko Cassiopeia
|
||||||
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Moko-Cassiopeia.Scripts
|
||||||
|
# INGROUP: Scripts.Git
|
||||||
|
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
# FILE: ./scripts/git/pre-commit.sh
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Pre-commit hook for local validation
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
. "${SCRIPT_DIR}/lib/common.sh"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}✓${NC} $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}⚠${NC} $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}✗${NC} $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_header() {
|
||||||
|
echo ""
|
||||||
|
echo "================================"
|
||||||
|
echo "$*"
|
||||||
|
echo "================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
SKIP_TESTS=false
|
||||||
|
SKIP_QUALITY=false
|
||||||
|
QUICK_MODE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--skip-tests)
|
||||||
|
SKIP_TESTS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-quality)
|
||||||
|
SKIP_QUALITY=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--quick)
|
||||||
|
QUICK_MODE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Usage: pre-commit.sh [--skip-tests] [--skip-quality] [--quick]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log_header "Pre-commit Validation"
|
||||||
|
|
||||||
|
# Get list of staged files
|
||||||
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
|
||||||
|
|
||||||
|
if [ -z "$STAGED_FILES" ]; then
|
||||||
|
log_warning "No staged files to check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking staged files:"
|
||||||
|
echo "$STAGED_FILES" | sed 's/^/ - /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Track failures
|
||||||
|
FAILURES=0
|
||||||
|
|
||||||
|
# Check 1: PHP Syntax
|
||||||
|
log_header "Checking PHP Syntax"
|
||||||
|
PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true)
|
||||||
|
|
||||||
|
if [ -n "$PHP_FILES" ]; then
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
if php -l "$file" > /dev/null 2>&1; then
|
||||||
|
log_success "PHP syntax OK: $file"
|
||||||
|
else
|
||||||
|
log_error "PHP syntax error: $file"
|
||||||
|
php -l "$file"
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$PHP_FILES"
|
||||||
|
else
|
||||||
|
echo " No PHP files to check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2: XML Well-formedness
|
||||||
|
log_header "Checking XML Files"
|
||||||
|
XML_FILES=$(echo "$STAGED_FILES" | grep '\.xml$' || true)
|
||||||
|
|
||||||
|
if [ -n "$XML_FILES" ]; then
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
if xmllint --noout "$file" 2>/dev/null; then
|
||||||
|
log_success "XML well-formed: $file"
|
||||||
|
else
|
||||||
|
log_error "XML malformed: $file"
|
||||||
|
xmllint --noout "$file" || true
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$XML_FILES"
|
||||||
|
else
|
||||||
|
echo " No XML files to check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 3: YAML Syntax
|
||||||
|
log_header "Checking YAML Files"
|
||||||
|
YAML_FILES=$(echo "$STAGED_FILES" | grep -E '\.(yml|yaml)$' || true)
|
||||||
|
|
||||||
|
if [ -n "$YAML_FILES" ]; then
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
# Use printf to safely pass the file path, avoiding injection
|
||||||
|
if python3 -c "import sys, yaml; yaml.safe_load(open(sys.argv[1]))" "$file" 2>/dev/null; then
|
||||||
|
log_success "YAML valid: $file"
|
||||||
|
else
|
||||||
|
log_error "YAML invalid: $file"
|
||||||
|
python3 -c "import sys, yaml; yaml.safe_load(open(sys.argv[1]))" "$file" || true
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$YAML_FILES"
|
||||||
|
else
|
||||||
|
echo " No YAML files to check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 4: Trailing Whitespace
|
||||||
|
log_header "Checking for Trailing Whitespace"
|
||||||
|
TEXT_FILES=$(echo "$STAGED_FILES" | grep -vE '\.(png|jpg|jpeg|gif|svg|ico|zip|gz|woff|woff2|ttf)$' || true)
|
||||||
|
|
||||||
|
if [ -n "$TEXT_FILES" ]; then
|
||||||
|
TRAILING_WS=$(echo "$TEXT_FILES" | xargs grep -n '[[:space:]]$' 2>/dev/null || true)
|
||||||
|
if [ -n "$TRAILING_WS" ]; then
|
||||||
|
log_warning "Files with trailing whitespace found:"
|
||||||
|
echo "$TRAILING_WS" | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
echo " Run: sed -i 's/[[:space:]]*$//' <file> to fix"
|
||||||
|
else
|
||||||
|
log_success "No trailing whitespace"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No text files to check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 5: SPDX License Headers (if not quick mode)
|
||||||
|
if [ "$QUICK_MODE" = false ]; then
|
||||||
|
log_header "Checking SPDX License Headers"
|
||||||
|
SOURCE_FILES=$(echo "$STAGED_FILES" | grep -E '\.(php|sh|js|ts|css)$' || true)
|
||||||
|
|
||||||
|
if [ -n "$SOURCE_FILES" ]; then
|
||||||
|
MISSING_SPDX=""
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
if ! head -n 20 "$file" | grep -q 'SPDX-License-Identifier:'; then
|
||||||
|
MISSING_SPDX="${MISSING_SPDX} - ${file}\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$SOURCE_FILES"
|
||||||
|
|
||||||
|
if [ -n "$MISSING_SPDX" ]; then
|
||||||
|
log_warning "Files missing SPDX license header:"
|
||||||
|
echo -e "$MISSING_SPDX"
|
||||||
|
else
|
||||||
|
log_success "All source files have SPDX headers"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No source files to check"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 6: No Secrets
|
||||||
|
log_header "Checking for Secrets"
|
||||||
|
if [ -x "${SCRIPT_DIR}/validate/no_secrets.sh" ]; then
|
||||||
|
if "${SCRIPT_DIR}/validate/no_secrets.sh" > /dev/null 2>&1; then
|
||||||
|
log_success "No secrets detected"
|
||||||
|
else
|
||||||
|
log_error "Potential secrets detected!"
|
||||||
|
"${SCRIPT_DIR}/validate/no_secrets.sh" || true
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Secret scanner not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 7: PHP_CodeSniffer (if not skipped)
|
||||||
|
if [ "$SKIP_QUALITY" = false ] && command -v phpcs >/dev/null 2>&1; then
|
||||||
|
log_header "Running PHP_CodeSniffer"
|
||||||
|
PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true)
|
||||||
|
|
||||||
|
if [ -n "$PHP_FILES" ]; then
|
||||||
|
# Use process substitution to avoid issues with filenames containing spaces
|
||||||
|
if echo "$PHP_FILES" | tr '\n' '\0' | xargs -0 phpcs --standard=phpcs.xml -q 2>/dev/null; then
|
||||||
|
log_success "PHPCS passed"
|
||||||
|
else
|
||||||
|
log_warning "PHPCS found issues (non-blocking)"
|
||||||
|
echo "$PHP_FILES" | tr '\n' '\0' | xargs -0 phpcs --standard=phpcs.xml --report=summary || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No PHP files to check"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$SKIP_QUALITY" = true ]; then
|
||||||
|
echo " Skipping PHPCS (--skip-quality)"
|
||||||
|
else
|
||||||
|
echo " PHPCS not available (install with: composer global require squizlabs/php_codesniffer)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 8: PHPStan (if not skipped and not quick mode)
|
||||||
|
if [ "$SKIP_QUALITY" = false ] && [ "$QUICK_MODE" = false ] && command -v phpstan >/dev/null 2>&1; then
|
||||||
|
log_header "Running PHPStan"
|
||||||
|
PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true)
|
||||||
|
|
||||||
|
if [ -n "$PHP_FILES" ]; then
|
||||||
|
if phpstan analyse --configuration=phpstan.neon --no-progress > /dev/null 2>&1; then
|
||||||
|
log_success "PHPStan passed"
|
||||||
|
else
|
||||||
|
log_warning "PHPStan found issues (non-blocking)"
|
||||||
|
phpstan analyse --configuration=phpstan.neon --no-progress || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No PHP files to check"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$SKIP_QUALITY" = true ]; then
|
||||||
|
echo " Skipping PHPStan (--skip-quality)"
|
||||||
|
elif [ "$QUICK_MODE" = true ]; then
|
||||||
|
echo " Skipping PHPStan (--quick mode)"
|
||||||
|
else
|
||||||
|
echo " PHPStan not available (install with: composer global require phpstan/phpstan)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
log_header "Pre-commit Summary"
|
||||||
|
|
||||||
|
if [ $FAILURES -gt 0 ]; then
|
||||||
|
log_error "Pre-commit checks failed with $FAILURES error(s)"
|
||||||
|
echo ""
|
||||||
|
echo "To commit anyway, use: git commit --no-verify"
|
||||||
|
echo "To run quick checks only: ./scripts/git/pre-commit.sh --quick"
|
||||||
|
echo "To skip quality checks: ./scripts/git/pre-commit.sh --skip-quality"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
log_success "All pre-commit checks passed!"
|
||||||
|
echo ""
|
||||||
|
echo "Tip: Use 'make validate' for comprehensive validation"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
452
scripts/lib/common.py
Executable file
452
scripts/lib/common.py
Executable file
@@ -0,0 +1,452 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Common utilities for Moko-Cassiopeia scripts.
|
||||||
|
|
||||||
|
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: Common
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/lib/common.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Unified shared Python utilities for all CI and local scripts
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
import subprocess
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Environment and Detection
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def is_ci() -> bool:
|
||||||
|
"""Check if running in CI environment."""
|
||||||
|
return os.environ.get("CI", "").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def require_cmd(command: str) -> None:
|
||||||
|
"""
|
||||||
|
Ensure a required command is available.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Command name to check
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If command is not found
|
||||||
|
"""
|
||||||
|
if not shutil.which(command):
|
||||||
|
log_error(f"Required command not found: {command}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Logging
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class Colors:
|
||||||
|
"""ANSI color codes for terminal output."""
|
||||||
|
RED = '\033[0;31m'
|
||||||
|
GREEN = '\033[0;32m'
|
||||||
|
YELLOW = '\033[1;33m'
|
||||||
|
BLUE = '\033[0;34m'
|
||||||
|
CYAN = '\033[0;36m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
NC = '\033[0m' # No Color
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enabled(cls) -> bool:
|
||||||
|
"""Check if colors should be enabled."""
|
||||||
|
return sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||||||
|
|
||||||
|
|
||||||
|
def log_info(message: str) -> None:
|
||||||
|
"""Log informational message."""
|
||||||
|
print(f"INFO: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_warn(message: str) -> None:
|
||||||
|
"""Log warning message."""
|
||||||
|
color = Colors.YELLOW if Colors.enabled() else ""
|
||||||
|
nc = Colors.NC if Colors.enabled() else ""
|
||||||
|
print(f"{color}WARN: {message}{nc}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def log_error(message: str) -> None:
|
||||||
|
"""Log error message."""
|
||||||
|
color = Colors.RED if Colors.enabled() else ""
|
||||||
|
nc = Colors.NC if Colors.enabled() else ""
|
||||||
|
print(f"{color}ERROR: {message}{nc}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def log_success(message: str) -> None:
|
||||||
|
"""Log success message."""
|
||||||
|
color = Colors.GREEN if Colors.enabled() else ""
|
||||||
|
nc = Colors.NC if Colors.enabled() else ""
|
||||||
|
print(f"{color}✓ {message}{nc}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_step(message: str) -> None:
|
||||||
|
"""Log a step in a process."""
|
||||||
|
color = Colors.CYAN if Colors.enabled() else ""
|
||||||
|
nc = Colors.NC if Colors.enabled() else ""
|
||||||
|
print(f"{color}➜ {message}{nc}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_section(title: str) -> None:
|
||||||
|
"""Log a section header."""
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(title)
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
def log_kv(key: str, value: str) -> None:
|
||||||
|
"""Log a key-value pair."""
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
def die(message: str, exit_code: int = 1) -> None:
|
||||||
|
"""
|
||||||
|
Log error and exit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Error message
|
||||||
|
exit_code: Exit code (default: 1)
|
||||||
|
"""
|
||||||
|
log_error(message)
|
||||||
|
|
||||||
|
if os.environ.get("VERBOSE_ERRORS", "true").lower() == "true":
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
print("Stack trace:", file=sys.stderr)
|
||||||
|
traceback.print_stack(file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
print("Environment:", file=sys.stderr)
|
||||||
|
print(f" PWD: {os.getcwd()}", file=sys.stderr)
|
||||||
|
print(f" USER: {os.environ.get('USER', 'unknown')}", file=sys.stderr)
|
||||||
|
print(f" PYTHON: {sys.version}", file=sys.stderr)
|
||||||
|
print(f" CI: {is_ci()}", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Validation Helpers
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def assert_file_exists(path: Union[str, Path]) -> None:
|
||||||
|
"""
|
||||||
|
Assert that a file exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to file
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If file doesn't exist
|
||||||
|
"""
|
||||||
|
if not Path(path).is_file():
|
||||||
|
die(f"Required file missing: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_dir_exists(path: Union[str, Path]) -> None:
|
||||||
|
"""
|
||||||
|
Assert that a directory exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to directory
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If directory doesn't exist
|
||||||
|
"""
|
||||||
|
if not Path(path).is_dir():
|
||||||
|
die(f"Required directory missing: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_not_empty(value: Any, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Assert that a value is not empty.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Value to check
|
||||||
|
name: Name of the value for error message
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If value is empty
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
die(f"Required value is empty: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# JSON Utilities
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def json_escape(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Escape text for JSON.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to escape
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Escaped text
|
||||||
|
"""
|
||||||
|
return json.dumps(text)[1:-1] # Remove surrounding quotes
|
||||||
|
|
||||||
|
|
||||||
|
def json_output(data: Dict[str, Any], pretty: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Output data as JSON.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Dictionary to output
|
||||||
|
pretty: Whether to pretty-print
|
||||||
|
"""
|
||||||
|
if pretty:
|
||||||
|
print(json.dumps(data, indent=2, sort_keys=True))
|
||||||
|
else:
|
||||||
|
print(json.dumps(data, separators=(',', ':')))
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Path Utilities
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def script_root() -> Path:
|
||||||
|
"""
|
||||||
|
Get the root scripts directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to scripts directory
|
||||||
|
"""
|
||||||
|
return Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
"""
|
||||||
|
Get the repository root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to repository root
|
||||||
|
"""
|
||||||
|
return script_root().parent
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_path(path: Union[str, Path]) -> str:
|
||||||
|
"""
|
||||||
|
Normalize a path (resolve, absolute, forward slashes).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to normalize
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalized path string
|
||||||
|
"""
|
||||||
|
return str(Path(path).resolve()).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# File Operations
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def read_file(path: Union[str, Path], encoding: str = "utf-8") -> str:
|
||||||
|
"""
|
||||||
|
Read a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to file
|
||||||
|
encoding: File encoding
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
File contents
|
||||||
|
"""
|
||||||
|
assert_file_exists(path)
|
||||||
|
return Path(path).read_text(encoding=encoding)
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(path: Union[str, Path], content: str, encoding: str = "utf-8") -> None:
|
||||||
|
"""
|
||||||
|
Write a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to file
|
||||||
|
content: Content to write
|
||||||
|
encoding: File encoding
|
||||||
|
"""
|
||||||
|
Path(path).write_text(content, encoding=encoding)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dir(path: Union[str, Path]) -> None:
|
||||||
|
"""
|
||||||
|
Ensure a directory exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to directory
|
||||||
|
"""
|
||||||
|
Path(path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Command Execution
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def run_command(
|
||||||
|
cmd: List[str],
|
||||||
|
capture_output: bool = True,
|
||||||
|
check: bool = True,
|
||||||
|
cwd: Optional[Union[str, Path]] = None,
|
||||||
|
env: Optional[Dict[str, str]] = None
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
"""
|
||||||
|
Run a command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Command and arguments
|
||||||
|
capture_output: Whether to capture stdout/stderr
|
||||||
|
check: Whether to raise on non-zero exit
|
||||||
|
cwd: Working directory
|
||||||
|
env: Environment variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletedProcess instance
|
||||||
|
"""
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=capture_output,
|
||||||
|
text=True,
|
||||||
|
check=check,
|
||||||
|
cwd=cwd,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_shell(
|
||||||
|
script: str,
|
||||||
|
capture_output: bool = True,
|
||||||
|
check: bool = True,
|
||||||
|
cwd: Optional[Union[str, Path]] = None
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
"""
|
||||||
|
Run a shell script.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
script: Shell script
|
||||||
|
capture_output: Whether to capture stdout/stderr
|
||||||
|
check: Whether to raise on non-zero exit
|
||||||
|
cwd: Working directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletedProcess instance
|
||||||
|
"""
|
||||||
|
return subprocess.run(
|
||||||
|
script,
|
||||||
|
shell=True,
|
||||||
|
capture_output=capture_output,
|
||||||
|
text=True,
|
||||||
|
check=check,
|
||||||
|
cwd=cwd
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Git Utilities
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def git_root() -> Path:
|
||||||
|
"""
|
||||||
|
Get git repository root.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to git root
|
||||||
|
"""
|
||||||
|
result = run_command(
|
||||||
|
["git", "rev-parse", "--show-toplevel"],
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return Path(result.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def git_status(porcelain: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Get git status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
porcelain: Use porcelain format
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Git status output
|
||||||
|
"""
|
||||||
|
cmd = ["git", "status"]
|
||||||
|
if porcelain:
|
||||||
|
cmd.append("--porcelain")
|
||||||
|
|
||||||
|
result = run_command(cmd, capture_output=True, check=True)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def git_branch() -> str:
|
||||||
|
"""
|
||||||
|
Get current git branch.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Branch name
|
||||||
|
"""
|
||||||
|
result = run_command(
|
||||||
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Entry Point (for testing)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Test the common utilities."""
|
||||||
|
log_section("Testing Common Utilities")
|
||||||
|
|
||||||
|
log_info("This is an info message")
|
||||||
|
log_warn("This is a warning message")
|
||||||
|
log_success("This is a success message")
|
||||||
|
log_step("This is a step message")
|
||||||
|
|
||||||
|
log_section("Environment")
|
||||||
|
log_kv("CI", str(is_ci()))
|
||||||
|
log_kv("Script Root", str(script_root()))
|
||||||
|
log_kv("Repo Root", str(repo_root()))
|
||||||
|
log_kv("Git Root", str(git_root()))
|
||||||
|
log_kv("Git Branch", git_branch())
|
||||||
|
|
||||||
|
log_section("Tests Passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
430
scripts/lib/joomla_manifest.py
Executable file
430
scripts/lib/joomla_manifest.py
Executable file
@@ -0,0 +1,430 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Joomla manifest parsing and validation utilities.
|
||||||
|
|
||||||
|
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: Joomla.Manifest
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/lib/joomla_manifest.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Joomla manifest parsing and validation utilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import common
|
||||||
|
except ImportError:
|
||||||
|
import common
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Joomla Extension Types
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class ExtensionType:
|
||||||
|
"""Joomla extension types."""
|
||||||
|
COMPONENT = "component"
|
||||||
|
MODULE = "module"
|
||||||
|
PLUGIN = "plugin"
|
||||||
|
TEMPLATE = "template"
|
||||||
|
LIBRARY = "library"
|
||||||
|
PACKAGE = "package"
|
||||||
|
FILE = "file"
|
||||||
|
LANGUAGE = "language"
|
||||||
|
|
||||||
|
ALL_TYPES = [
|
||||||
|
COMPONENT, MODULE, PLUGIN, TEMPLATE,
|
||||||
|
LIBRARY, PACKAGE, FILE, LANGUAGE
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Manifest Data Class
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ManifestInfo:
|
||||||
|
"""Information extracted from a Joomla manifest."""
|
||||||
|
path: Path
|
||||||
|
extension_type: str
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
author: Optional[str] = None
|
||||||
|
author_email: Optional[str] = None
|
||||||
|
author_url: Optional[str] = None
|
||||||
|
copyright: Optional[str] = None
|
||||||
|
license: Optional[str] = None
|
||||||
|
creation_date: Optional[str] = None
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, str]:
|
||||||
|
"""Convert to dictionary."""
|
||||||
|
return {
|
||||||
|
"path": str(self.path),
|
||||||
|
"ext_type": self.extension_type,
|
||||||
|
"name": self.name,
|
||||||
|
"version": self.version,
|
||||||
|
"description": self.description or "",
|
||||||
|
"author": self.author or "",
|
||||||
|
"author_email": self.author_email or "",
|
||||||
|
"author_url": self.author_url or "",
|
||||||
|
"copyright": self.copyright or "",
|
||||||
|
"license": self.license or "",
|
||||||
|
"creation_date": self.creation_date or ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Manifest Discovery
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def find_manifest(src_dir: str = "src") -> Path:
|
||||||
|
"""
|
||||||
|
Find the primary Joomla manifest in the given directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_dir: Source directory to search
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to manifest file
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If no manifest found
|
||||||
|
"""
|
||||||
|
src_path = Path(src_dir)
|
||||||
|
|
||||||
|
if not src_path.is_dir():
|
||||||
|
common.die(f"Source directory missing: {src_dir}")
|
||||||
|
|
||||||
|
# Template manifest (templateDetails.xml)
|
||||||
|
template_manifest = src_path / "templateDetails.xml"
|
||||||
|
if template_manifest.is_file():
|
||||||
|
return template_manifest
|
||||||
|
|
||||||
|
# Also check in templates subdirectory
|
||||||
|
templates_dir = src_path / "templates"
|
||||||
|
if templates_dir.is_dir():
|
||||||
|
for template_file in templates_dir.glob("templateDetails.xml"):
|
||||||
|
return template_file
|
||||||
|
|
||||||
|
# Package manifest (pkg_*.xml)
|
||||||
|
pkg_manifests = list(src_path.rglob("pkg_*.xml"))
|
||||||
|
if pkg_manifests:
|
||||||
|
return pkg_manifests[0]
|
||||||
|
|
||||||
|
# Component manifest (com_*.xml)
|
||||||
|
com_manifests = list(src_path.rglob("com_*.xml"))
|
||||||
|
if com_manifests:
|
||||||
|
return com_manifests[0]
|
||||||
|
|
||||||
|
# Module manifest (mod_*.xml)
|
||||||
|
mod_manifests = list(src_path.rglob("mod_*.xml"))
|
||||||
|
if mod_manifests:
|
||||||
|
return mod_manifests[0]
|
||||||
|
|
||||||
|
# Plugin manifest (plg_*.xml)
|
||||||
|
plg_manifests = list(src_path.rglob("plg_*.xml"))
|
||||||
|
if plg_manifests:
|
||||||
|
return plg_manifests[0]
|
||||||
|
|
||||||
|
# Fallback: any XML with <extension
|
||||||
|
for xml_file in src_path.rglob("*.xml"):
|
||||||
|
try:
|
||||||
|
content = xml_file.read_text(encoding="utf-8")
|
||||||
|
if "<extension" in content:
|
||||||
|
return xml_file
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
common.die(f"No Joomla manifest XML found under {src_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_all_manifests(src_dir: str = "src") -> List[Path]:
|
||||||
|
"""
|
||||||
|
Find all Joomla manifests in the given directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_dir: Source directory to search
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of manifest paths
|
||||||
|
"""
|
||||||
|
src_path = Path(src_dir)
|
||||||
|
|
||||||
|
if not src_path.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
manifests = []
|
||||||
|
|
||||||
|
# Template manifests
|
||||||
|
manifests.extend(src_path.rglob("templateDetails.xml"))
|
||||||
|
|
||||||
|
# Package manifests
|
||||||
|
manifests.extend(src_path.rglob("pkg_*.xml"))
|
||||||
|
|
||||||
|
# Component manifests
|
||||||
|
manifests.extend(src_path.rglob("com_*.xml"))
|
||||||
|
|
||||||
|
# Module manifests
|
||||||
|
manifests.extend(src_path.rglob("mod_*.xml"))
|
||||||
|
|
||||||
|
# Plugin manifests
|
||||||
|
manifests.extend(src_path.rglob("plg_*.xml"))
|
||||||
|
|
||||||
|
return manifests
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Manifest Parsing
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def parse_manifest(manifest_path: Path) -> ManifestInfo:
|
||||||
|
"""
|
||||||
|
Parse a Joomla manifest file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest_path: Path to manifest file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ManifestInfo object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SystemExit: If parsing fails
|
||||||
|
"""
|
||||||
|
if not manifest_path.is_file():
|
||||||
|
common.die(f"Manifest not found: {manifest_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ET.parse(manifest_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Extract extension type
|
||||||
|
ext_type = root.attrib.get("type", "").strip().lower()
|
||||||
|
if not ext_type:
|
||||||
|
common.die(f"Manifest missing type attribute: {manifest_path}")
|
||||||
|
|
||||||
|
# Extract name
|
||||||
|
name_elem = root.find("name")
|
||||||
|
if name_elem is None or not name_elem.text:
|
||||||
|
common.die(f"Manifest missing name element: {manifest_path}")
|
||||||
|
name = name_elem.text.strip()
|
||||||
|
|
||||||
|
# Extract version
|
||||||
|
version_elem = root.find("version")
|
||||||
|
if version_elem is None or not version_elem.text:
|
||||||
|
common.die(f"Manifest missing version element: {manifest_path}")
|
||||||
|
version = version_elem.text.strip()
|
||||||
|
|
||||||
|
# Extract optional fields
|
||||||
|
description = None
|
||||||
|
desc_elem = root.find("description")
|
||||||
|
if desc_elem is not None and desc_elem.text:
|
||||||
|
description = desc_elem.text.strip()
|
||||||
|
|
||||||
|
author = None
|
||||||
|
author_elem = root.find("author")
|
||||||
|
if author_elem is not None and author_elem.text:
|
||||||
|
author = author_elem.text.strip()
|
||||||
|
|
||||||
|
author_email = None
|
||||||
|
email_elem = root.find("authorEmail")
|
||||||
|
if email_elem is not None and email_elem.text:
|
||||||
|
author_email = email_elem.text.strip()
|
||||||
|
|
||||||
|
author_url = None
|
||||||
|
url_elem = root.find("authorUrl")
|
||||||
|
if url_elem is not None and url_elem.text:
|
||||||
|
author_url = url_elem.text.strip()
|
||||||
|
|
||||||
|
copyright_text = None
|
||||||
|
copyright_elem = root.find("copyright")
|
||||||
|
if copyright_elem is not None and copyright_elem.text:
|
||||||
|
copyright_text = copyright_elem.text.strip()
|
||||||
|
|
||||||
|
license_text = None
|
||||||
|
license_elem = root.find("license")
|
||||||
|
if license_elem is not None and license_elem.text:
|
||||||
|
license_text = license_elem.text.strip()
|
||||||
|
|
||||||
|
creation_date = None
|
||||||
|
date_elem = root.find("creationDate")
|
||||||
|
if date_elem is not None and date_elem.text:
|
||||||
|
creation_date = date_elem.text.strip()
|
||||||
|
|
||||||
|
return ManifestInfo(
|
||||||
|
path=manifest_path,
|
||||||
|
extension_type=ext_type,
|
||||||
|
name=name,
|
||||||
|
version=version,
|
||||||
|
description=description,
|
||||||
|
author=author,
|
||||||
|
author_email=author_email,
|
||||||
|
author_url=author_url,
|
||||||
|
copyright=copyright_text,
|
||||||
|
license=license_text,
|
||||||
|
creation_date=creation_date
|
||||||
|
)
|
||||||
|
|
||||||
|
except ET.ParseError as e:
|
||||||
|
common.die(f"Failed to parse manifest {manifest_path}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
common.die(f"Error reading manifest {manifest_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_manifest_version(manifest_path: Path) -> str:
|
||||||
|
"""
|
||||||
|
Extract version from manifest.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest_path: Path to manifest file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Version string
|
||||||
|
"""
|
||||||
|
info = parse_manifest(manifest_path)
|
||||||
|
return info.version
|
||||||
|
|
||||||
|
|
||||||
|
def get_manifest_name(manifest_path: Path) -> str:
|
||||||
|
"""
|
||||||
|
Extract name from manifest.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest_path: Path to manifest file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Name string
|
||||||
|
"""
|
||||||
|
info = parse_manifest(manifest_path)
|
||||||
|
return info.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_manifest_type(manifest_path: Path) -> str:
|
||||||
|
"""
|
||||||
|
Extract extension type from manifest.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest_path: Path to manifest file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Extension type string
|
||||||
|
"""
|
||||||
|
info = parse_manifest(manifest_path)
|
||||||
|
return info.extension_type
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Manifest Validation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def validate_manifest(manifest_path: Path) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Validate a Joomla manifest.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest_path: Path to manifest file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_warnings)
|
||||||
|
"""
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = parse_manifest(manifest_path)
|
||||||
|
|
||||||
|
# Check for recommended fields
|
||||||
|
if not info.description:
|
||||||
|
warnings.append("Missing description element")
|
||||||
|
|
||||||
|
if not info.author:
|
||||||
|
warnings.append("Missing author element")
|
||||||
|
|
||||||
|
if not info.copyright:
|
||||||
|
warnings.append("Missing copyright element")
|
||||||
|
|
||||||
|
if not info.license:
|
||||||
|
warnings.append("Missing license element")
|
||||||
|
|
||||||
|
if not info.creation_date:
|
||||||
|
warnings.append("Missing creationDate element")
|
||||||
|
|
||||||
|
# Validate extension type
|
||||||
|
if info.extension_type not in ExtensionType.ALL_TYPES:
|
||||||
|
warnings.append(f"Unknown extension type: {info.extension_type}")
|
||||||
|
|
||||||
|
return (True, warnings)
|
||||||
|
|
||||||
|
except SystemExit:
|
||||||
|
return (False, ["Failed to parse manifest"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Entry Point (for testing)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Test the manifest utilities."""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
common.log_section("Testing Joomla Manifest Utilities")
|
||||||
|
|
||||||
|
src_dir = sys.argv[1] if len(sys.argv) > 1 else "src"
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = find_manifest(src_dir)
|
||||||
|
common.log_success(f"Found manifest: {manifest}")
|
||||||
|
|
||||||
|
info = parse_manifest(manifest)
|
||||||
|
|
||||||
|
common.log_section("Manifest Information")
|
||||||
|
common.log_kv("Type", info.extension_type)
|
||||||
|
common.log_kv("Name", info.name)
|
||||||
|
common.log_kv("Version", info.version)
|
||||||
|
|
||||||
|
if info.description:
|
||||||
|
common.log_kv("Description", info.description[:60] + "..." if len(info.description) > 60 else info.description)
|
||||||
|
|
||||||
|
if info.author:
|
||||||
|
common.log_kv("Author", info.author)
|
||||||
|
|
||||||
|
is_valid, warnings = validate_manifest(manifest)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
common.log_success("Manifest is valid")
|
||||||
|
if warnings:
|
||||||
|
common.log_warn(f"Warnings: {len(warnings)}")
|
||||||
|
for warning in warnings:
|
||||||
|
print(f" - {warning}")
|
||||||
|
else:
|
||||||
|
common.log_error("Manifest validation failed")
|
||||||
|
|
||||||
|
except SystemExit as e:
|
||||||
|
sys.exit(e.code)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
312
scripts/release/package_extension.py
Executable file
312
scripts/release/package_extension.py
Executable file
@@ -0,0 +1,312 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Package Joomla extension as distributable ZIP.
|
||||||
|
|
||||||
|
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.Packaging
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/release/package_extension.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Package Joomla extension as distributable ZIP
|
||||||
|
USAGE: ./scripts/release/package_extension.py [output_dir] [version]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
|
# Add lib directory to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import common
|
||||||
|
import joomla_manifest
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Exclusion patterns for packaging
|
||||||
|
EXCLUDE_PATTERNS = {
|
||||||
|
# Version control
|
||||||
|
".git", ".gitignore", ".gitattributes",
|
||||||
|
# IDE
|
||||||
|
".vscode", ".idea", "*.sublime-*",
|
||||||
|
# Development
|
||||||
|
"node_modules", "vendor", ".env", ".env.*",
|
||||||
|
# Documentation (optional, can be included)
|
||||||
|
# Build artifacts
|
||||||
|
"dist", "build", ".phpunit.cache",
|
||||||
|
# OS files
|
||||||
|
".DS_Store", "Thumbs.db",
|
||||||
|
# Logs
|
||||||
|
"*.log",
|
||||||
|
# Tests
|
||||||
|
"tests", "test", "Tests",
|
||||||
|
# CI/CD
|
||||||
|
".github",
|
||||||
|
# Scripts
|
||||||
|
"scripts",
|
||||||
|
# Docs (can be included if needed)
|
||||||
|
"docs",
|
||||||
|
# Config files
|
||||||
|
"composer.json", "composer.lock",
|
||||||
|
"package.json", "package-lock.json",
|
||||||
|
"phpunit.xml", "phpstan.neon", "phpcs.xml",
|
||||||
|
"codeception.yml",
|
||||||
|
# Others
|
||||||
|
"README.md", "CHANGELOG.md", "CONTRIBUTING.md",
|
||||||
|
"CODE_OF_CONDUCT.md", "SECURITY.md", "GOVERNANCE.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def should_exclude(path: Path, base_path: Path, exclude_patterns: Set[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a path should be excluded from packaging.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to check
|
||||||
|
base_path: Base directory path
|
||||||
|
exclude_patterns: Set of exclusion patterns
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if should be excluded
|
||||||
|
"""
|
||||||
|
relative_path = path.relative_to(base_path)
|
||||||
|
|
||||||
|
# Check each part of the path
|
||||||
|
for part in relative_path.parts:
|
||||||
|
if part in exclude_patterns:
|
||||||
|
return True
|
||||||
|
# Check wildcard patterns
|
||||||
|
for pattern in exclude_patterns:
|
||||||
|
if "*" in pattern:
|
||||||
|
import fnmatch
|
||||||
|
if fnmatch.fnmatch(part, pattern):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_package(
|
||||||
|
src_dir: str,
|
||||||
|
output_dir: str,
|
||||||
|
version: str = None,
|
||||||
|
repo_name: str = None,
|
||||||
|
exclude_patterns: Set[str] = None
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Create a distributable ZIP package for a Joomla extension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_dir: Source directory containing extension files
|
||||||
|
output_dir: Output directory for ZIP file
|
||||||
|
version: Version string (auto-detected if not provided)
|
||||||
|
repo_name: Repository name for ZIP file naming
|
||||||
|
exclude_patterns: Patterns to exclude from packaging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to created ZIP file
|
||||||
|
"""
|
||||||
|
src_path = Path(src_dir)
|
||||||
|
if not src_path.is_dir():
|
||||||
|
common.die(f"Source directory not found: {src_dir}")
|
||||||
|
|
||||||
|
# Find and parse manifest
|
||||||
|
manifest_path = joomla_manifest.find_manifest(src_dir)
|
||||||
|
manifest_info = joomla_manifest.parse_manifest(manifest_path)
|
||||||
|
|
||||||
|
# Determine version
|
||||||
|
if not version:
|
||||||
|
version = manifest_info.version
|
||||||
|
|
||||||
|
# Determine repo name
|
||||||
|
if not repo_name:
|
||||||
|
try:
|
||||||
|
repo_root = common.git_root()
|
||||||
|
repo_name = repo_root.name
|
||||||
|
except Exception:
|
||||||
|
repo_name = "extension"
|
||||||
|
|
||||||
|
# Determine exclusion patterns
|
||||||
|
if exclude_patterns is None:
|
||||||
|
exclude_patterns = EXCLUDE_PATTERNS
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
output_path = Path(output_dir)
|
||||||
|
common.ensure_dir(output_path)
|
||||||
|
|
||||||
|
# Generate ZIP filename
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
zip_filename = f"{repo_name}-{version}-{manifest_info.extension_type}.zip"
|
||||||
|
zip_path = output_path / zip_filename
|
||||||
|
|
||||||
|
# Remove existing ZIP if present
|
||||||
|
if zip_path.exists():
|
||||||
|
zip_path.unlink()
|
||||||
|
|
||||||
|
common.log_section("Creating Extension Package")
|
||||||
|
common.log_kv("Extension", manifest_info.name)
|
||||||
|
common.log_kv("Type", manifest_info.extension_type)
|
||||||
|
common.log_kv("Version", version)
|
||||||
|
common.log_kv("Source", src_dir)
|
||||||
|
common.log_kv("Output", str(zip_path))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create ZIP file
|
||||||
|
file_count = 0
|
||||||
|
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
for item in src_path.rglob("*"):
|
||||||
|
if item.is_file():
|
||||||
|
# Check if should be excluded
|
||||||
|
if should_exclude(item, src_path, exclude_patterns):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add to ZIP with relative path
|
||||||
|
arcname = item.relative_to(src_path)
|
||||||
|
zipf.write(item, arcname)
|
||||||
|
file_count += 1
|
||||||
|
|
||||||
|
if file_count % 10 == 0:
|
||||||
|
common.log_step(f"Added {file_count} files...")
|
||||||
|
|
||||||
|
# Get ZIP file size
|
||||||
|
zip_size = zip_path.stat().st_size
|
||||||
|
zip_size_mb = zip_size / (1024 * 1024)
|
||||||
|
|
||||||
|
print()
|
||||||
|
common.log_success(f"Package created: {zip_path.name}")
|
||||||
|
common.log_kv("Files", str(file_count))
|
||||||
|
common.log_kv("Size", f"{zip_size_mb:.2f} MB")
|
||||||
|
|
||||||
|
# Output JSON for machine consumption
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"extension": manifest_info.name,
|
||||||
|
"ext_type": manifest_info.extension_type,
|
||||||
|
"version": version,
|
||||||
|
"package": str(zip_path),
|
||||||
|
"files": file_count,
|
||||||
|
"size_bytes": zip_size
|
||||||
|
}
|
||||||
|
|
||||||
|
print()
|
||||||
|
common.json_output(result)
|
||||||
|
|
||||||
|
return zip_path
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Package Joomla extension as distributable ZIP",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Package with auto-detected version
|
||||||
|
%(prog)s
|
||||||
|
|
||||||
|
# Package to specific directory
|
||||||
|
%(prog)s dist
|
||||||
|
|
||||||
|
# Package with specific version
|
||||||
|
%(prog)s dist 1.2.3
|
||||||
|
|
||||||
|
# Package with custom source
|
||||||
|
%(prog)s --src-dir my-extension dist 1.0.0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"output_dir",
|
||||||
|
nargs="?",
|
||||||
|
default="dist",
|
||||||
|
help="Output directory for ZIP file (default: dist)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"version",
|
||||||
|
nargs="?",
|
||||||
|
help="Version string (default: auto-detected from manifest)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--src-dir",
|
||||||
|
default="src",
|
||||||
|
help="Source directory (default: src)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo-name",
|
||||||
|
help="Repository name for ZIP filename (default: auto-detected)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-docs",
|
||||||
|
action="store_true",
|
||||||
|
help="Include documentation files in package"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-tests",
|
||||||
|
action="store_true",
|
||||||
|
help="Include test files in package"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Adjust exclusion patterns based on arguments
|
||||||
|
exclude_patterns = EXCLUDE_PATTERNS.copy()
|
||||||
|
if args.include_docs:
|
||||||
|
exclude_patterns.discard("docs")
|
||||||
|
exclude_patterns.discard("README.md")
|
||||||
|
exclude_patterns.discard("CHANGELOG.md")
|
||||||
|
if args.include_tests:
|
||||||
|
exclude_patterns.discard("tests")
|
||||||
|
exclude_patterns.discard("test")
|
||||||
|
exclude_patterns.discard("Tests")
|
||||||
|
|
||||||
|
# Create package
|
||||||
|
zip_path = create_package(
|
||||||
|
src_dir=args.src_dir,
|
||||||
|
output_dir=args.output_dir,
|
||||||
|
version=args.version,
|
||||||
|
repo_name=args.repo_name,
|
||||||
|
exclude_patterns=exclude_patterns
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
common.log_error(f"Packaging failed: {e}")
|
||||||
|
result = {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
common.json_output(result)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
448
scripts/run/scaffold_extension.py
Executable file
448
scripts/run/scaffold_extension.py
Executable file
@@ -0,0 +1,448 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create Joomla extension scaffolding.
|
||||||
|
|
||||||
|
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: Moko-Cassiopeia.Scripts
|
||||||
|
INGROUP: Scripts.Run
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/run/scaffold_extension.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Create scaffolding for different Joomla extension types
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
# Add lib directory to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import common
|
||||||
|
import joomla_manifest
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Templates for Extension Scaffolding
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_component_structure(name: str, description: str, author: str) -> Dict[str, str]:
|
||||||
|
"""Get directory structure and files for a component."""
|
||||||
|
safe_name = name.lower().replace(" ", "_")
|
||||||
|
com_name = f"com_{safe_name}"
|
||||||
|
|
||||||
|
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<extension type="component" version="4.0" method="upgrade">
|
||||||
|
<name>{name}</name>
|
||||||
|
<author>{author}</author>
|
||||||
|
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
|
||||||
|
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<authorEmail>hello@example.com</authorEmail>
|
||||||
|
<authorUrl>https://example.com</authorUrl>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>{description}</description>
|
||||||
|
|
||||||
|
<files folder="site">
|
||||||
|
<folder>src</folder>
|
||||||
|
</files>
|
||||||
|
|
||||||
|
<administration>
|
||||||
|
<menu>{name}</menu>
|
||||||
|
<files folder="admin">
|
||||||
|
<folder>services</folder>
|
||||||
|
<folder>sql</folder>
|
||||||
|
<folder>src</folder>
|
||||||
|
</files>
|
||||||
|
</administration>
|
||||||
|
</extension>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
f"{com_name}.xml": manifest,
|
||||||
|
"site/src/.gitkeep": "",
|
||||||
|
"admin/services/provider.php": f"<?php\n// Service provider for {name}\n",
|
||||||
|
"admin/sql/install.mysql.utf8.sql": "-- Installation SQL\n",
|
||||||
|
"admin/sql/uninstall.mysql.utf8.sql": "-- Uninstallation SQL\n",
|
||||||
|
"admin/src/.gitkeep": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_structure(name: str, description: str, author: str, client: str = "site") -> Dict[str, str]:
|
||||||
|
"""Get directory structure and files for a module."""
|
||||||
|
safe_name = name.lower().replace(" ", "_")
|
||||||
|
mod_name = f"mod_{safe_name}"
|
||||||
|
|
||||||
|
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<extension type="module" version="4.0" client="{client}" method="upgrade">
|
||||||
|
<name>{name}</name>
|
||||||
|
<author>{author}</author>
|
||||||
|
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
|
||||||
|
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<authorEmail>hello@example.com</authorEmail>
|
||||||
|
<authorUrl>https://example.com</authorUrl>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>{description}</description>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<filename module="{mod_name}">{mod_name}.php</filename>
|
||||||
|
<filename>{mod_name}.xml</filename>
|
||||||
|
<folder>tmpl</folder>
|
||||||
|
</files>
|
||||||
|
</extension>
|
||||||
|
"""
|
||||||
|
|
||||||
|
module_php = f"""<?php
|
||||||
|
/**
|
||||||
|
* @package {name}
|
||||||
|
* @copyright Copyright (C) {datetime.now().year} {author}
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
// Module logic here
|
||||||
|
require JModuleHelper::getLayoutPath('mod_{safe_name}', $params->get('layout', 'default'));
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_tmpl = f"""<?php
|
||||||
|
/**
|
||||||
|
* @package {name}
|
||||||
|
* @copyright Copyright (C) {datetime.now().year} {author}
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="{mod_name}">
|
||||||
|
<p><?php echo JText::_('MOD_{safe_name.upper()}_DESCRIPTION'); ?></p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
f"{mod_name}.xml": manifest,
|
||||||
|
f"{mod_name}.php": module_php,
|
||||||
|
"tmpl/default.php": default_tmpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_structure(name: str, description: str, author: str, group: str = "system") -> Dict[str, str]:
|
||||||
|
"""Get directory structure and files for a plugin."""
|
||||||
|
safe_name = name.lower().replace(" ", "_")
|
||||||
|
plg_name = f"{safe_name}"
|
||||||
|
|
||||||
|
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<extension type="plugin" version="4.0" group="{group}" method="upgrade">
|
||||||
|
<name>plg_{group}_{safe_name}</name>
|
||||||
|
<author>{author}</author>
|
||||||
|
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
|
||||||
|
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<authorEmail>hello@example.com</authorEmail>
|
||||||
|
<authorUrl>https://example.com</authorUrl>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>{description}</description>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<filename plugin="{plg_name}">{plg_name}.php</filename>
|
||||||
|
</files>
|
||||||
|
</extension>
|
||||||
|
"""
|
||||||
|
|
||||||
|
plugin_php = f"""<?php
|
||||||
|
/**
|
||||||
|
* @package {name}
|
||||||
|
* @copyright Copyright (C) {datetime.now().year} {author}
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\\CMS\\Plugin\\CMSPlugin;
|
||||||
|
|
||||||
|
class Plg{group.capitalize()}{plg_name.capitalize()} extends CMSPlugin
|
||||||
|
{{
|
||||||
|
protected $autoloadLanguage = true;
|
||||||
|
|
||||||
|
public function onContentPrepare($context, &$article, &$params, $limitstart = 0)
|
||||||
|
{{
|
||||||
|
// Plugin logic here
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
f"plg_{group}_{safe_name}.xml": manifest,
|
||||||
|
f"{plg_name}.php": plugin_php,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_structure(name: str, description: str, author: str) -> Dict[str, str]:
|
||||||
|
"""Get directory structure and files for a template."""
|
||||||
|
safe_name = name.lower().replace(" ", "_")
|
||||||
|
|
||||||
|
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<extension type="template" version="4.0" client="site" method="upgrade">
|
||||||
|
<name>{safe_name}</name>
|
||||||
|
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
|
||||||
|
<author>{author}</author>
|
||||||
|
<authorEmail>hello@example.com</authorEmail>
|
||||||
|
<authorUrl>https://example.com</authorUrl>
|
||||||
|
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>{description}</description>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<filename>index.php</filename>
|
||||||
|
<filename>templateDetails.xml</filename>
|
||||||
|
<folder>css</folder>
|
||||||
|
<folder>js</folder>
|
||||||
|
<folder>images</folder>
|
||||||
|
</files>
|
||||||
|
|
||||||
|
<positions>
|
||||||
|
<position>header</position>
|
||||||
|
<position>main</position>
|
||||||
|
<position>footer</position>
|
||||||
|
</positions>
|
||||||
|
</extension>
|
||||||
|
"""
|
||||||
|
|
||||||
|
index_php = f"""<?php
|
||||||
|
/**
|
||||||
|
* @package {name}
|
||||||
|
* @copyright Copyright (C) {datetime.now().year} {author}
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\\CMS\\Factory;
|
||||||
|
use Joomla\\CMS\\HTML\\HTMLHelper;
|
||||||
|
use Joomla\\CMS\\Uri\\Uri;
|
||||||
|
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
$wa = $app->getDocument()->getWebAssetManager();
|
||||||
|
|
||||||
|
// Load template assets
|
||||||
|
$wa->useStyle('template.{safe_name}')->useScript('template.{safe_name}');
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
|
||||||
|
<head>
|
||||||
|
<jdoc:include type="metas" />
|
||||||
|
<jdoc:include type="styles" />
|
||||||
|
<jdoc:include type="scripts" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<jdoc:include type="modules" name="header" style="html5" />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<jdoc:include type="component" />
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<jdoc:include type="modules" name="footer" style="html5" />
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"templateDetails.xml": manifest,
|
||||||
|
"index.php": index_php,
|
||||||
|
"css/template.css": "/* Template styles */\n",
|
||||||
|
"js/template.js": "// Template JavaScript\n",
|
||||||
|
"images/.gitkeep": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_structure(name: str, description: str, author: str) -> Dict[str, str]:
|
||||||
|
"""Get directory structure and files for a package."""
|
||||||
|
safe_name = name.lower().replace(" ", "_")
|
||||||
|
pkg_name = f"pkg_{safe_name}"
|
||||||
|
|
||||||
|
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<extension type="package" version="4.0" method="upgrade">
|
||||||
|
<name>{name}</name>
|
||||||
|
<packagename>{safe_name}</packagename>
|
||||||
|
<author>{author}</author>
|
||||||
|
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
|
||||||
|
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<authorEmail>hello@example.com</authorEmail>
|
||||||
|
<authorUrl>https://example.com</authorUrl>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>{description}</description>
|
||||||
|
|
||||||
|
<files folder="packages">
|
||||||
|
<!-- Add extension packages here -->
|
||||||
|
</files>
|
||||||
|
</extension>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
f"{pkg_name}.xml": manifest,
|
||||||
|
"packages/.gitkeep": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Scaffolding Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def create_extension(
|
||||||
|
ext_type: str,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
author: str,
|
||||||
|
output_dir: str = "src",
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Create extension scaffolding.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ext_type: Extension type (component, module, plugin, template, package)
|
||||||
|
name: Extension name
|
||||||
|
description: Extension description
|
||||||
|
author: Author name
|
||||||
|
output_dir: Output directory
|
||||||
|
**kwargs: Additional type-specific options
|
||||||
|
"""
|
||||||
|
output_path = Path(output_dir)
|
||||||
|
|
||||||
|
# Get structure based on type
|
||||||
|
if ext_type == "component":
|
||||||
|
structure = get_component_structure(name, description, author)
|
||||||
|
elif ext_type == "module":
|
||||||
|
client = kwargs.get("client", "site")
|
||||||
|
structure = get_module_structure(name, description, author, client)
|
||||||
|
elif ext_type == "plugin":
|
||||||
|
group = kwargs.get("group", "system")
|
||||||
|
structure = get_plugin_structure(name, description, author, group)
|
||||||
|
elif ext_type == "template":
|
||||||
|
structure = get_template_structure(name, description, author)
|
||||||
|
elif ext_type == "package":
|
||||||
|
structure = get_package_structure(name, description, author)
|
||||||
|
else:
|
||||||
|
common.die(f"Unknown extension type: {ext_type}")
|
||||||
|
|
||||||
|
# Create files
|
||||||
|
common.log_section(f"Creating {ext_type}: {name}")
|
||||||
|
|
||||||
|
for file_path, content in structure.items():
|
||||||
|
full_path = output_path / file_path
|
||||||
|
|
||||||
|
# Create parent directories
|
||||||
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Write file
|
||||||
|
full_path.write_text(content, encoding="utf-8")
|
||||||
|
common.log_success(f"Created: {file_path}")
|
||||||
|
|
||||||
|
common.log_section("Scaffolding Complete")
|
||||||
|
common.log_info(f"Extension files created in: {output_path}")
|
||||||
|
common.log_info(f"Extension type: {ext_type}")
|
||||||
|
common.log_info(f"Extension name: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Command Line Interface
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Create Joomla extension scaffolding",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Create a component
|
||||||
|
%(prog)s component MyComponent "My Component Description" "John Doe"
|
||||||
|
|
||||||
|
# Create a module
|
||||||
|
%(prog)s module MyModule "My Module Description" "John Doe" --client site
|
||||||
|
|
||||||
|
# Create a plugin
|
||||||
|
%(prog)s plugin MyPlugin "My Plugin Description" "John Doe" --group system
|
||||||
|
|
||||||
|
# Create a template
|
||||||
|
%(prog)s template mytheme "My Theme Description" "John Doe"
|
||||||
|
|
||||||
|
# Create a package
|
||||||
|
%(prog)s package mypackage "My Package Description" "John Doe"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"type",
|
||||||
|
choices=["component", "module", "plugin", "template", "package"],
|
||||||
|
help="Extension type to create"
|
||||||
|
)
|
||||||
|
parser.add_argument("name", help="Extension name")
|
||||||
|
parser.add_argument("description", help="Extension description")
|
||||||
|
parser.add_argument("author", help="Author name")
|
||||||
|
parser.add_argument(
|
||||||
|
"-o", "--output",
|
||||||
|
default="src",
|
||||||
|
help="Output directory (default: src)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--client",
|
||||||
|
choices=["site", "administrator"],
|
||||||
|
default="site",
|
||||||
|
help="Module client (site or administrator)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--group",
|
||||||
|
default="system",
|
||||||
|
help="Plugin group (system, content, user, etc.)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
create_extension(
|
||||||
|
ext_type=args.type,
|
||||||
|
name=args.name,
|
||||||
|
description=args.description,
|
||||||
|
author=args.author,
|
||||||
|
output_dir=args.output,
|
||||||
|
client=args.client,
|
||||||
|
group=args.group
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
common.log_error(f"Failed to create extension: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
168
scripts/validate/manifest.py
Executable file
168
scripts/validate/manifest.py
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Validate Joomla manifest files.
|
||||||
|
|
||||||
|
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: Moko-Cassiopeia.Scripts
|
||||||
|
INGROUP: Scripts.Validate
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/validate/manifest.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Validate Joomla extension manifest files
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 common
|
||||||
|
import joomla_manifest
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_manifest_file(manifest_path: Path, verbose: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Validate a single manifest file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest_path: Path to manifest file
|
||||||
|
verbose: Show detailed output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
info = joomla_manifest.parse_manifest(manifest_path)
|
||||||
|
is_valid, warnings = joomla_manifest.validate_manifest(manifest_path)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
common.log_section(f"Manifest: {manifest_path}")
|
||||||
|
common.log_kv("Type", info.extension_type)
|
||||||
|
common.log_kv("Name", info.name)
|
||||||
|
common.log_kv("Version", info.version)
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
common.log_warn(f"Warnings ({len(warnings)}):")
|
||||||
|
for warning in warnings:
|
||||||
|
print(f" - {warning}")
|
||||||
|
|
||||||
|
# Output JSON for machine consumption
|
||||||
|
result = {
|
||||||
|
"status": "ok" if is_valid else "error",
|
||||||
|
"manifest": str(manifest_path),
|
||||||
|
"ext_type": info.extension_type,
|
||||||
|
"name": info.name,
|
||||||
|
"version": info.version,
|
||||||
|
"warnings": warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
if not verbose:
|
||||||
|
common.json_output(result)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
if not verbose:
|
||||||
|
print(f"manifest: ok ({manifest_path})")
|
||||||
|
else:
|
||||||
|
common.log_success("Manifest is valid")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
common.log_error(f"Manifest validation failed: {manifest_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except SystemExit:
|
||||||
|
common.log_error(f"Failed to parse manifest: {manifest_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Validate Joomla extension manifest files",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--src-dir",
|
||||||
|
default="src",
|
||||||
|
help="Source directory to search for manifests (default: src)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Show detailed output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"manifest",
|
||||||
|
nargs="?",
|
||||||
|
help="Specific manifest file to validate (optional)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.manifest:
|
||||||
|
# Validate specific manifest
|
||||||
|
manifest_path = Path(args.manifest)
|
||||||
|
if not manifest_path.is_file():
|
||||||
|
common.die(f"Manifest file not found: {args.manifest}")
|
||||||
|
|
||||||
|
success = validate_manifest_file(manifest_path, args.verbose)
|
||||||
|
return 0 if success else 1
|
||||||
|
else:
|
||||||
|
# Find and validate all manifests in src directory
|
||||||
|
manifests = joomla_manifest.find_all_manifests(args.src_dir)
|
||||||
|
|
||||||
|
if not manifests:
|
||||||
|
common.die(f"No manifest files found in {args.src_dir}")
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
common.log_section("Validating Manifests")
|
||||||
|
common.log_info(f"Found {len(manifests)} manifest(s)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
all_valid = True
|
||||||
|
for manifest in manifests:
|
||||||
|
if not validate_manifest_file(manifest, args.verbose):
|
||||||
|
all_valid = False
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
print()
|
||||||
|
if all_valid:
|
||||||
|
common.log_success(f"All {len(manifests)} manifest(s) are valid")
|
||||||
|
else:
|
||||||
|
common.log_error("Some manifests failed validation")
|
||||||
|
|
||||||
|
return 0 if all_valid else 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
common.log_error(f"Validation failed: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
218
scripts/validate/php_syntax.py
Executable file
218
scripts/validate/php_syntax.py
Executable file
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Validate PHP syntax in files.
|
||||||
|
|
||||||
|
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: Moko-Cassiopeia.Scripts
|
||||||
|
INGROUP: Scripts.Validate
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/validate/php_syntax.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Validate PHP syntax in all PHP files
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
# Add lib directory to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import common
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def check_php_file(file_path: Path) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Check PHP syntax of a single file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to PHP file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, error_message)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["php", "-l", str(file_path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return (True, "")
|
||||||
|
else:
|
||||||
|
return (False, result.stderr or result.stdout)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return (False, "Syntax check timed out")
|
||||||
|
except Exception as e:
|
||||||
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def find_php_files(src_dir: str, exclude_dirs: List[str] = None) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Find all PHP files in a directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_dir: Directory to search
|
||||||
|
exclude_dirs: Directories to exclude
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PHP file paths
|
||||||
|
"""
|
||||||
|
if exclude_dirs is None:
|
||||||
|
exclude_dirs = ["vendor", "node_modules", ".git"]
|
||||||
|
|
||||||
|
src_path = Path(src_dir)
|
||||||
|
if not src_path.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
php_files = []
|
||||||
|
for php_file in src_path.rglob("*.php"):
|
||||||
|
# Check if file is in an excluded directory
|
||||||
|
if any(excluded in php_file.parts for excluded in exclude_dirs):
|
||||||
|
continue
|
||||||
|
php_files.append(php_file)
|
||||||
|
|
||||||
|
return sorted(php_files)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Validate PHP syntax in all PHP files",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--src-dir",
|
||||||
|
default="src",
|
||||||
|
help="Source directory to search for PHP files (default: src)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Show detailed output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exclude",
|
||||||
|
action="append",
|
||||||
|
help="Directories to exclude (can be specified multiple times)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"files",
|
||||||
|
nargs="*",
|
||||||
|
help="Specific files to check (optional)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Check if PHP is available
|
||||||
|
common.require_cmd("php")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine which files to check
|
||||||
|
if args.files:
|
||||||
|
php_files = [Path(f) for f in args.files]
|
||||||
|
for f in php_files:
|
||||||
|
if not f.is_file():
|
||||||
|
common.die(f"File not found: {f}")
|
||||||
|
else:
|
||||||
|
exclude_dirs = args.exclude or ["vendor", "node_modules", ".git"]
|
||||||
|
php_files = find_php_files(args.src_dir, exclude_dirs)
|
||||||
|
|
||||||
|
if not php_files:
|
||||||
|
common.die(f"No PHP files found in {args.src_dir}")
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
common.log_section("PHP Syntax Validation")
|
||||||
|
common.log_info(f"Checking {len(php_files)} PHP file(s)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for php_file in php_files:
|
||||||
|
is_valid, error_msg = check_php_file(php_file)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
if args.verbose:
|
||||||
|
common.log_success(f"OK: {php_file}")
|
||||||
|
else:
|
||||||
|
errors.append((php_file, error_msg))
|
||||||
|
if args.verbose:
|
||||||
|
common.log_error(f"FAILED: {php_file}")
|
||||||
|
if error_msg:
|
||||||
|
print(f" {error_msg}")
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
if args.verbose:
|
||||||
|
print()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
result = {
|
||||||
|
"status": "error",
|
||||||
|
"total": len(php_files),
|
||||||
|
"passed": len(php_files) - len(errors),
|
||||||
|
"failed": len(errors),
|
||||||
|
"errors": [{"file": str(f), "error": e} for f, e in errors]
|
||||||
|
}
|
||||||
|
|
||||||
|
if not args.verbose:
|
||||||
|
common.json_output(result)
|
||||||
|
|
||||||
|
common.log_error(f"PHP syntax check failed: {len(errors)} error(s)")
|
||||||
|
|
||||||
|
if not args.verbose:
|
||||||
|
for file_path, error_msg in errors:
|
||||||
|
print(f"ERROR: {file_path}")
|
||||||
|
if error_msg:
|
||||||
|
print(f" {error_msg}")
|
||||||
|
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"total": len(php_files),
|
||||||
|
"passed": len(php_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not args.verbose:
|
||||||
|
common.json_output(result)
|
||||||
|
print(f"php_syntax: ok ({len(php_files)} file(s) checked)")
|
||||||
|
else:
|
||||||
|
common.log_success(f"All {len(php_files)} PHP file(s) are valid")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
common.log_error(f"Validation failed: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
206
scripts/validate/xml_wellformed.py
Executable file
206
scripts/validate/xml_wellformed.py
Executable file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Validate XML well-formedness.
|
||||||
|
|
||||||
|
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: Moko-Cassiopeia.Scripts
|
||||||
|
INGROUP: Scripts.Validate
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/validate/xml_wellformed.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Validate XML well-formedness in all XML files
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
# Add lib directory to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import common
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def check_xml_file(file_path: Path) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Check if an XML file is well-formed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to XML file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, error_message)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ET.parse(file_path)
|
||||||
|
return (True, "")
|
||||||
|
except ET.ParseError as e:
|
||||||
|
return (False, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def find_xml_files(src_dir: str, exclude_dirs: List[str] = None) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Find all XML files in a directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_dir: Directory to search
|
||||||
|
exclude_dirs: Directories to exclude
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of XML file paths
|
||||||
|
"""
|
||||||
|
if exclude_dirs is None:
|
||||||
|
exclude_dirs = ["vendor", "node_modules", ".git"]
|
||||||
|
|
||||||
|
src_path = Path(src_dir)
|
||||||
|
if not src_path.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
xml_files = []
|
||||||
|
for xml_file in src_path.rglob("*.xml"):
|
||||||
|
# Check if file is in an excluded directory
|
||||||
|
if any(excluded in xml_file.parts for excluded in exclude_dirs):
|
||||||
|
continue
|
||||||
|
xml_files.append(xml_file)
|
||||||
|
|
||||||
|
return sorted(xml_files)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Validate XML well-formedness in all XML files",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--src-dir",
|
||||||
|
default="src",
|
||||||
|
help="Source directory to search for XML files (default: src)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Show detailed output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exclude",
|
||||||
|
action="append",
|
||||||
|
help="Directories to exclude (can be specified multiple times)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"files",
|
||||||
|
nargs="*",
|
||||||
|
help="Specific files to check (optional)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine which files to check
|
||||||
|
if args.files:
|
||||||
|
xml_files = [Path(f) for f in args.files]
|
||||||
|
for f in xml_files:
|
||||||
|
if not f.is_file():
|
||||||
|
common.die(f"File not found: {f}")
|
||||||
|
else:
|
||||||
|
exclude_dirs = args.exclude or ["vendor", "node_modules", ".git"]
|
||||||
|
xml_files = find_xml_files(args.src_dir, exclude_dirs)
|
||||||
|
|
||||||
|
if not xml_files:
|
||||||
|
common.die(f"No XML files found in {args.src_dir}")
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
common.log_section("XML Well-formedness Validation")
|
||||||
|
common.log_info(f"Checking {len(xml_files)} XML file(s)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for xml_file in xml_files:
|
||||||
|
is_valid, error_msg = check_xml_file(xml_file)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
if args.verbose:
|
||||||
|
common.log_success(f"OK: {xml_file}")
|
||||||
|
else:
|
||||||
|
errors.append((xml_file, error_msg))
|
||||||
|
if args.verbose:
|
||||||
|
common.log_error(f"FAILED: {xml_file}")
|
||||||
|
if error_msg:
|
||||||
|
print(f" {error_msg}")
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
if args.verbose:
|
||||||
|
print()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
result = {
|
||||||
|
"status": "error",
|
||||||
|
"src_dir": args.src_dir,
|
||||||
|
"xml_count": len(xml_files),
|
||||||
|
"passed": len(xml_files) - len(errors),
|
||||||
|
"failed": len(errors),
|
||||||
|
"errors": [{"file": str(f), "error": e} for f, e in errors]
|
||||||
|
}
|
||||||
|
|
||||||
|
if not args.verbose:
|
||||||
|
common.json_output(result)
|
||||||
|
|
||||||
|
common.log_error(f"XML validation failed: {len(errors)} error(s)")
|
||||||
|
|
||||||
|
if not args.verbose:
|
||||||
|
for file_path, error_msg in errors:
|
||||||
|
print(f"ERROR: {file_path}")
|
||||||
|
if error_msg:
|
||||||
|
print(f" {error_msg}")
|
||||||
|
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"src_dir": args.src_dir,
|
||||||
|
"xml_count": len(xml_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not args.verbose:
|
||||||
|
common.json_output(result)
|
||||||
|
print(f"xml_wellformed: ok")
|
||||||
|
else:
|
||||||
|
common.log_success(f"All {len(xml_files)} XML file(s) are well-formed")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
common.log_error(f"Validation failed: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user