From 5ce8874f06c135bf05ca3850326174e6c5fd3bb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:20:00 +0000 Subject: [PATCH 1/8] Initial plan -- 2.49.1 From e1c7f54fecbb16c39ab8632fc2498048ef78b6ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:27:23 +0000 Subject: [PATCH 2/8] feat: add development workflow improvements - Add comprehensive workflow documentation (WORKFLOW_GUIDE.md) - Add quick start guide (QUICK_START.md) - Add caching to GitHub Actions workflows for faster CI/CD - Create Makefile with common development tasks - Add pre-commit hook script for local validation - Add VS Code tasks configuration - Add git hooks installation script - Update .gitignore to allow VS Code config files Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- .github/workflows/joomla_testing.yml | 49 ++- .github/workflows/php_quality.yml | 30 ++ .gitignore | 6 +- .vscode/tasks.json | 171 ++++++++++ Makefile | 193 +++++++++++ docs/QUICK_START.md | 352 ++++++++++++++++++++ docs/WORKFLOW_GUIDE.md | 472 +++++++++++++++++++++++++++ scripts/git/install-hooks.sh | 56 ++++ scripts/git/pre-commit.sh | 270 +++++++++++++++ 9 files changed, 1590 insertions(+), 9 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 Makefile create mode 100644 docs/QUICK_START.md create mode 100644 docs/WORKFLOW_GUIDE.md create mode 100755 scripts/git/install-hooks.sh create mode 100755 scripts/git/pre-commit.sh diff --git a/.github/workflows/joomla_testing.yml b/.github/workflows/joomla_testing.yml index 8c6a9b9..800f4b0 100644 --- a/.github/workflows/joomla_testing.yml +++ b/.github/workflows/joomla_testing.yml @@ -56,23 +56,46 @@ jobs: uses: actions/setup-node@v4 with: 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 }} run: | 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 - wget -q https://downloads.joomla.org/cms/joomla4/4-4-9/Joomla_4-4-9-Stable-Full_Package.zip - unzip -q Joomla_4-4-9-Stable-Full_Package.zip + ZIP_FILE="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 - wget -q https://downloads.joomla.org/cms/joomla5/5-0-3/Joomla_5-0-3-Stable-Full_Package.zip - unzip -q Joomla_5-0-3-Stable-Full_Package.zip + ZIP_FILE="Joomla_5-0-3-Stable-Full_Package.zip" + ZIP_URL="https://downloads.joomla.org/cms/joomla5/5-0-3/${ZIP_FILE}" else - wget -q https://downloads.joomla.org/cms/joomla5/5-1-4/Joomla_5-1-4-Stable-Full_Package.zip - unzip -q Joomla_5-1-4-Stable-Full_Package.zip + ZIP_FILE="Joomla_5-1-4-Stable-Full_Package.zip" + ZIP_URL="https://downloads.joomla.org/cms/joomla5/5-1-4/${ZIP_FILE}" 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 run: | @@ -211,6 +234,16 @@ jobs: coverage: xdebug 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 run: | composer global require codeception/codeception diff --git a/.github/workflows/php_quality.yml b/.github/workflows/php_quality.yml index 22b70a2..6d9bd49 100644 --- a/.github/workflows/php_quality.yml +++ b/.github/workflows/php_quality.yml @@ -38,6 +38,16 @@ jobs: coverage: none 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 run: | composer global require squizlabs/php_codesniffer @@ -82,6 +92,16 @@ jobs: extensions: mbstring, xml, ctype, json, zip 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 run: | composer global require phpstan/phpstan @@ -119,6 +139,16 @@ jobs: extensions: mbstring, xml, ctype, json, zip 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 run: | composer global require squizlabs/php_codesniffer diff --git a/.gitignore b/.gitignore index 71f055d..90279ad 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,11 @@ System Volume Information/ *.lnk Icon? .idea/ -.vscode/ +# .vscode/ - Allow specific VS Code config files +.vscode/* +!.vscode/tasks.json +!.vscode/settings.json.example +!.vscode/extensions.json *.code-workspace *.sublime-project *.sublime-workspace diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..071585d --- /dev/null +++ b/.vscode/tasks.json @@ -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": [] + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..634f298 --- /dev/null +++ b/Makefile @@ -0,0 +1,193 @@ +# Makefile for Moko Cassiopeia Development +# Copyright (C) 2025 Moko Consulting +# +# 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 '\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:" + @find scripts/run -type f -name "*.sh" -exec basename {} \; | sort + +## 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" diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md new file mode 100644 index 0000000..00031ff --- /dev/null +++ b/docs/QUICK_START.md @@ -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:** 2026-01-04 +**Get Started:** Run `make dev-setup` now! diff --git a/docs/WORKFLOW_GUIDE.md b/docs/WORKFLOW_GUIDE.md new file mode 100644 index 0000000..b1c9c0c --- /dev/null +++ b/docs/WORKFLOW_GUIDE.md @@ -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 --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:** 2026-01-04 +**Maintained by:** Moko Consulting Engineering diff --git a/scripts/git/install-hooks.sh b/scripts/git/install-hooks.sh new file mode 100755 index 0000000..c2307bb --- /dev/null +++ b/scripts/git/install-hooks.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Install Git hooks for Moko Cassiopeia +# Copyright (C) 2025 Moko Consulting +# +# 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!" diff --git a/scripts/git/pre-commit.sh b/scripts/git/pre-commit.sh new file mode 100755 index 0000000..18ed667 --- /dev/null +++ b/scripts/git/pre-commit.sh @@ -0,0 +1,270 @@ +#!/usr/bin/env bash +# Pre-commit hook script for Moko Cassiopeia +# Copyright (C) 2025 Moko Consulting +# +# 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 + if python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null; then + log_success "YAML valid: $file" + else + log_error "YAML invalid: $file" + python3 -c "import yaml; yaml.safe_load(open('$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:]]*$//' 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 + if echo "$PHP_FILES" | xargs phpcs --standard=phpcs.xml -q 2>/dev/null; then + log_success "PHPCS passed" + else + log_warning "PHPCS found issues (non-blocking)" + echo "$PHP_FILES" | xargs 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 -- 2.49.1 From 15205433c9b796a4b731819f264a82a1949b74f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:29:20 +0000 Subject: [PATCH 3/8] fix: address code review feedback - Fix command injection vulnerabilities in pre-commit script - Use proper file path quoting for YAML validation - Use xargs -0 for safe filename handling - Fix incorrect date (2026 -> 2025) in documentation - Add scripts/git to allowed directories in repo_health workflow Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- .github/workflows/repo_health.yml | 2 +- docs/QUICK_START.md | 2 +- docs/WORKFLOW_GUIDE.md | 2 +- scripts/git/pre-commit.sh | 10 ++++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml index 6165a14..8c24c9d 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -36,7 +36,7 @@ env: # Scripts governance policy # Note: directories listed without a trailing slash. 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 # Files are listed as-is; directories must end with a trailing slash. diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md index 00031ff..96a98ed 100644 --- a/docs/QUICK_START.md +++ b/docs/QUICK_START.md @@ -348,5 +348,5 @@ make help # Show all commands --- **Document Version:** 1.0.0 -**Last Updated:** 2026-01-04 +**Last Updated:** 2025-01-04 **Get Started:** Run `make dev-setup` now! diff --git a/docs/WORKFLOW_GUIDE.md b/docs/WORKFLOW_GUIDE.md index b1c9c0c..f39baed 100644 --- a/docs/WORKFLOW_GUIDE.md +++ b/docs/WORKFLOW_GUIDE.md @@ -468,5 +468,5 @@ phpcs --standard=phpcs.xml --report=source src/ --- **Document Version:** 1.0.0 -**Last Updated:** 2026-01-04 +**Last Updated:** 2025-01-04 **Maintained by:** Moko Consulting Engineering diff --git a/scripts/git/pre-commit.sh b/scripts/git/pre-commit.sh index 18ed667..673548c 100755 --- a/scripts/git/pre-commit.sh +++ b/scripts/git/pre-commit.sh @@ -133,11 +133,12 @@ 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 - if python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null; 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 yaml; yaml.safe_load(open('$file'))" || true + python3 -c "import sys, yaml; yaml.safe_load(open(sys.argv[1]))" "$file" || true FAILURES=$((FAILURES + 1)) fi fi @@ -210,11 +211,12 @@ if [ "$SKIP_QUALITY" = false ] && command -v phpcs >/dev/null 2>&1; then PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true) if [ -n "$PHP_FILES" ]; then - if echo "$PHP_FILES" | xargs phpcs --standard=phpcs.xml -q 2>/dev/null; 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" | xargs phpcs --standard=phpcs.xml --report=summary || true + echo "$PHP_FILES" | tr '\n' '\0' | xargs -0 phpcs --standard=phpcs.xml --report=summary || true fi else echo " No PHP files to check" -- 2.49.1 From 70f3a494eecd9d6329897aa1ed3f5fe60266e7d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:31:01 +0000 Subject: [PATCH 4/8] docs: update README and CONTRIBUTING with new development tools - Add Quick Start section to README with make commands - Add Development Tools section to CONTRIBUTING - Link to new documentation guides (QUICK_START.md, WORKFLOW_GUIDE.md) - Update development workflow section with modern tooling - Add clear onboarding path for new contributors Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- CONTRIBUTING.md | 31 +++++++++++++++++++++++ README.md | 65 ++++++++++++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e461eed..8aaa234 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,37 @@ Contributors are expected to: * Have a working understanding of Joomla template structure. * Be familiar with Git and GitHub pull request workflows. * 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 diff --git a/README.md b/README.md index 2f80254..d5c2e24 100644 --- a/README.md +++ b/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: +### 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 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 - **PHP Quality Checks** - PHPStan and PHP_CodeSniffer with Joomla standards - **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: -- 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 -``` +Run `make help` to see all available development commands. ## Changelog -- 2.49.1 From 4d8963fd2713a02d2bbfa31e7156b27a695e6cb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:40:29 +0000 Subject: [PATCH 5/8] feat: add Python library infrastructure and extension scaffolding - Add scripts/lib/common.py with core utilities (logging, validation, JSON, file ops, git) - Add scripts/lib/joomla_manifest.py for manifest parsing and validation - Add scripts/run/scaffold_extension.py to create extension scaffolding - Support all Joomla extension types (component, module, plugin, template, package) - Maintain CLI compatibility with existing bash scripts - Foundation for converting remaining scripts to Python Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- .../lib/__pycache__/common.cpython-312.pyc | Bin 0 -> 15773 bytes .../joomla_manifest.cpython-312.pyc | Bin 0 -> 14399 bytes scripts/lib/common.py | 452 ++++++++++++++++++ scripts/lib/joomla_manifest.py | 430 +++++++++++++++++ scripts/run/scaffold_extension.py | 448 +++++++++++++++++ 5 files changed, 1330 insertions(+) create mode 100644 scripts/lib/__pycache__/common.cpython-312.pyc create mode 100644 scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc create mode 100755 scripts/lib/common.py create mode 100755 scripts/lib/joomla_manifest.py create mode 100755 scripts/run/scaffold_extension.py diff --git a/scripts/lib/__pycache__/common.cpython-312.pyc b/scripts/lib/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..849226b727a322d0b50b77bc8b964c89964f993b GIT binary patch literal 15773 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
H@Dir<-Z#KK)rSF z!|?k*Vx9P}9=^5aL$qdCD;Lc84_ytWaSje5icA6K5Pt%3JlD83+;lJ`^!IloV2O8# z#~z5?)$R`MQYav%$n;|8v~+-OT*t&FNgKi6pcvaow>CKj=}-i!CzGnsRzo4}RmcNX zKA=}Gy;|wTD9dQPo<2o-6?s9sN*hB?(PtN4x)T?2jdPt>sO(jGMd)>bUKBXBv5R6b zEE1r+MLF7+>DA>PzF{cI_cGY~Ddor_GIlDBl%t>;TR7N8_=)$haj*KQ@|UO}qoZzx z(aSq|o_{Q?Ga)3jQ?zn4@=Ibnxpl(L6xcXC-Q}KJOR!Fcl%8A|&}rfj^W{spwD^ zc#%+1BjH6tc#*IoqhX`b5JfJ&jv6ANhDh?&4t_(%Nj+}JwzFjVICZ??v417MIs=qQ zrj2B#f04fO;Q-&9Jv-fsQ<)cL*EQuieBa-)NJ+jSz_(^QrZ-P()1ldhomAI)A0tAN zZwT_cs1qd97zL6Ui~>o1o1foAV?~mUE>iM%O@Kd;k+QAR&g{YWl7#2LjKq@9&^n0+p*P9M)s&en$r>%kd@_2A=8LB2K9k=>ltvZ2|! zEi{PM`NWokIMa2z9pk(+cU4d*F5Xnm?vcUAm^@r;1XwD;$j7TzsOr1KK9k4 zw_sJ?iInw()a;3J%k@O%{vyf%eZR;D`Nj<9gXHo0D!x7of+3j!rI6&e1o@4bcBwhD7e{upxb;25^M*js1dY`BO literal 0 HcmV?d00001 diff --git a/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc b/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f80db25d2fc872b45c0b7656967f41f76330ee3 GIT binary patch literal 14399 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 + +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() diff --git a/scripts/lib/joomla_manifest.py b/scripts/lib/joomla_manifest.py new file mode 100755 index 0000000..c7322d1 --- /dev/null +++ b/scripts/lib/joomla_manifest.py @@ -0,0 +1,430 @@ +#!/usr/bin/env python3 +""" +Joomla manifest parsing and validation utilities. + +Copyright (C) 2025 Moko Consulting + +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 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() diff --git a/scripts/run/scaffold_extension.py b/scripts/run/scaffold_extension.py new file mode 100755 index 0000000..19306f6 --- /dev/null +++ b/scripts/run/scaffold_extension.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +""" +Create Joomla extension scaffolding. + +Copyright (C) 2025 Moko Consulting + +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""" + + {name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + src + + + + {name} + + services + sql + src + + + +""" + + return { + f"{com_name}.xml": manifest, + "site/src/.gitkeep": "", + "admin/services/provider.php": f" Dict[str, str]: + """Get directory structure and files for a module.""" + safe_name = name.lower().replace(" ", "_") + mod_name = f"mod_{safe_name}" + + manifest = f""" + + {name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + {mod_name}.php + {mod_name}.xml + tmpl + + +""" + + module_php = f"""get('layout', 'default')); +""" + + default_tmpl = f""" +
+

+
+""" + + 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""" + + plg_{group}_{safe_name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + {plg_name}.php + + +""" + + plugin_php = f""" Dict[str, str]: + """Get directory structure and files for a template.""" + safe_name = name.lower().replace(" ", "_") + + manifest = f""" + + {safe_name} + {datetime.now().strftime("%Y-%m-%d")} + {author} + hello@example.com + https://example.com + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + 1.0.0 + {description} + + + index.php + templateDetails.xml + css + js + images + + + + header + main + footer + + +""" + + index_php = f"""getDocument()->getWebAssetManager(); + +// Load template assets +$wa->useStyle('template.{safe_name}')->useScript('template.{safe_name}'); +?> + + + + + + + + +
+ +
+
+ +
+
+ +
+ + +""" + + 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""" + + {name} + {safe_name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + + + +""" + + 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() -- 2.49.1 From 64f4b959f82cb3b08ea4255c4085f313cdf0205a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 04:41:54 +0000 Subject: [PATCH 6/8] Changes before error encountered Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- .gitignore | 30 +++++++++++++++ Makefile | 36 +++++++++++++++++- .../lib/__pycache__/common.cpython-312.pyc | Bin 15773 -> 0 bytes .../joomla_manifest.cpython-312.pyc | Bin 14399 -> 0 bytes 4 files changed, 65 insertions(+), 1 deletion(-) delete mode 100644 scripts/lib/__pycache__/common.cpython-312.pyc delete mode 100644 scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc diff --git a/.gitignore b/.gitignore index 90279ad..8d3812a 100644 --- a/.gitignore +++ b/.gitignore @@ -793,6 +793,36 @@ package-lock.json .phpunit.result.cache 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 # ============================================================ diff --git a/Makefile b/Makefile index 634f298..7e302f9 100644 --- a/Makefile +++ b/Makefile @@ -179,8 +179,42 @@ list-scripts: @echo "Available fix scripts:" @find scripts/fix -type f -name "*.sh" -exec basename {} \; | sort @echo "" - @echo "Available run scripts:" + @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= NAME= AUTHOR= DESC=" + @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: diff --git a/scripts/lib/__pycache__/common.cpython-312.pyc b/scripts/lib/__pycache__/common.cpython-312.pyc deleted file mode 100644 index 849226b727a322d0b50b77bc8b964c89964f993b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15773 zcmeHOTW}OtdhVX-xsR^MfIvWV6@!rkh?|E?EQA2DvcyWl1bJiCXu5?)%thPX64pfa z;##&0wl|E+t}w)SkvFcg2qa5YUi_5WR3(*%yo^@T!nR4WmH5RksUdPzs;E>Y-+#KN zr)MN%OBsWeQV)93u>Kj-|n^Pm45{e4A+pTjf#ujga$@8r0Dp^w5-X=WDJI63YH zCvgLu#7mAiKftrpG2mdSbHK?`VL(9YjJp!<0XI*uLfn(^4tNv30bjyD;7yMbqiNv}N^_w&*bsg)?p=(HELJ6YZ# z?L%#obX3}pv{`PE4qV*Maf;W$$<24oq4C_LW6}}&y~uw+LHzBhL?V?G$F*2Irp07c z98M|XOR0;gruK-c#!_Q)EF!8=B{rt1&3=H7O)9YqBbvCqJtXegwPzm#7TZ%vbv&-c zk{86oBXT^RdNBbUQLD6BlcOU?{r>)u7`lkXWfAFEMA5|5uozi_V@yen%25p+_MJKY z`%PW3sGL;grcOytYO&#%tc1mrXS$kNn|C#(l%{w@lNA#;T3%2h2|_=t$g-%WhP6u( zMLr}>rpCo+Bq=Jg6jL=NHZ-nb6q*=GN-dakA|=I!C;ccGPfD^PY9q1;v_63WwqI=PJ};hY>+Nmp?mvGBklIKJweo~);E5&1 z;xSAPq*NkFZ4zVlztqv&ehTny$2z+@`_E(8;)%}w?vB1b@kCFr*e0H7>+SDsKik#T zE1o&qd#0zaqgm_&RWN=3Q>aN2m`EwID9KtR7ALVg58kO5QCt#7A`>!r7?ooa7*9lu zLLv(k{CcGRNIV7Eqc6ErI-I&2kZT+1+-Tvbp zCr^92{9rE{foaqURBbqj*hFe-LpsVqr=4dL>QmP6~ zqzbI8DQc;h|4du|sj%2$YFbM?Hq?TqP|fDCN&m|oy?q!;SlqR{dDkxd_aE!+>^Kn? z&n8J5B+v#TE{SI*!M1W`j)3Xyoh0UBJQaoTntk}cB!kc$;&tInL>mdYbZ1*~QWuWL zqM9yrK}U3N50kz~Tz8?TR8n^HB~y}5lvj$RaF!=f^tOp?`syknAbaw4Xrk_niJX5G== zsdKuZDdTcT&>bmN_n7s%^Mb7Du6XK_tlZ%ismmuPTSig|xrOk_O3S5`ap>^?jg2=Sy3Keg~y`t1WG_-t8O_OPjPq6dUcCt(OcrDxDUCH9ECcb(=1w#dJ7#D zQ}D#Zf(64l=>*kh{34zXSb8XD1WNeB@feAnIbyQ8kqKyQWGF5-Gg$U&Q!c1smZRn- za2QJlo2QtJBw!L!Vw6~Ffw4?)B&N!?wtbVTCMP;xi)mr8bGU$t`c0-ZtX#0MtX;aH zw|s0CKv?{e=G6^(cdH|`?&+>eBeCd+?u(}`49bdMZ zuX!^yKMmwvTyP`fc!@HRBGI#W46hrU%)x8r$YX`K>VT)}9B@g(fLn46cqI3LSMm(_ zB=3M<4oE&Z2+zuYMJTAtK*eMr6x7}Asd!3J(_7oR`#M1~eBdpaD?P1nu_TNFto_)y z)|~dNeP!37*4+s+vxjANA8Ktal*$qYrwXmpQQIwM;wo2B3qSXcFk7yJKoZ(TZj)ESMdb_s!z2JxHC@9zU# zZN>|N;^wMXTu)z1Kd2ICtHimgO~0z@{6zUQ{c-x!H$Hyj{^(p|+b{cbjh#7fClizI zP^Io293*WS9Mt`TgK${JyGYrJrIp!Axg+tJS1HhN~Pk;Hkw^6y~rprxmQHW(d#6B z>T$fTaK8~+9{N{aZhshDeYxXdMg8Ry4+E<%A2(lh$oXn7AN#GVI`43}PV-se-#AJa zot(SoF)K1UAhfzCA**WSf?Og-0&18T5s2ONEj4KHYMtpdT`5`(;6x;tzXjPg(Ke^u zw62H6?mN8h8dG9P(r%`_HtqJ36=3ZTqQi6esWo`Pf(yQ@Co%^zp`5>AL0D(#t|?#u z;5ZUcHqzfBQ?d|JY1vZ{Zl&6$7&O%0x-+v2j+rHGK>a%iNf?3v;}2-i!mWsjj-f=R1=q{$7x+57?X}(xC4AaY)LtQE_qWA~+sU(bG&brOF zU%mP2{JN%hPh5ZL?U(NFx!mz+Wy6i+d&&8g+umJyz5ea`nPZoaJpvQYUOk!FpKZ+6 zPw$!DlJhs-i~eHt&gid&7yh56wZu}k1X@aCFx1o0+uPF{7L_o_UCPeqQBuZESCpzu&aDw|H{WcYU$X<8JN@?Q`;Mn`?m+fHHZ*-~dT-9Z{hs=Z^qust zh4522_e*s3zflr~@^R4$qxHL8`LtMVm zBElUVqpe4T_LIb#W~MF%OMZ5%j3u-kuxZLtmN4^1IF$~7{?NvcwHt3Ay?Jzg?epNr z_ul^A{n}^ehwjp1TD){|b)pOUEe_yiUt9~z6Z|e~i7Wb3_R`{R!(M8KWy|t&CczNZ zp`gcz96^w!IeqxZ@9=ah`!Q^+;U|#T{VzkbjjohasC8JxpqK=S6u^cTGg%<8su#fwfk5TEpz5GW-y%qgYKp63W!poHlSaF_egT#V~D$ zD5f27M2rbIluqDOPSYT%%ff2sCJntfp>(0_GF7f(8FEC9T~)8jnLQafb1vs!w;-%H z2JfRS%OJV;C1a=`9Tw(R*vJf_$~sDIZnyT7q7lkz2fc{Jh} zdq(RDxnf$NEp6$)6MnDu?ELl>Pcy!AzX985X=h#bd}q8tje_y)`JHL=EV5Zq=yh~W zAy-VNtSu;(@rqDodYr{4$QBx27DeSqXTLUFV*1r#v4a8=g_WOSfmh7hGJOXC(6F7o zxLuNmBZ$U_#oZwWsaGO`cntB^LFUHwDKu!dQZaun9qiL02&ZXEBr1o~!H!}SEu8j< z;+b>D!(!Siif8*e-~;LcrMhQ4c`=!~luVOR)(5C)(J*rseyrP1ha#OR`zB;8{M%2`BBSqBUu2iqkmFCkxIFr5;P23|(# zPpF263S*;P&ABSCUYHkFe&O-Gm3}k*)*ElWkz2WK#y#h0dEnVM>)AKwIq=#3IZxXo zZ^hN257Zm!_tFdAP4t!gr1jJA$KeHUGeS8Jyz6GY>ue=YgRaPUZaHrIZu&k9WM0oz zZoSw3i_>>bFL)0CZN_`6_V${aYi_Tk~; zw-4Pslx@mYnvU**H%t}o560iWl!@gk#ZOd<7c6-9&{&Hj|3dJ*b?VJiSI;j9tBiF) z7(;}2`E7MSNVWWuFlreffS6A~wbZJoMHblslQaEOCVXwGD&w4o9YREmvmZ#jO2!*w7AYuAaY5QZG9U~38^VI@cu7jhYO)b07}OPOo?c}I1sOylhV!lD1rk=L zLQ~Z}h}=>Y6QMi2!Z!c34w$>oT0}`O6$UeC%t3ht)j!8i4dG?P|Ee>>JK+a`b+dtW z^MMVB`GNh@()7!@;LZhaQ{Kr1#ib51Ex9y^#s5Zr+%jlg!Vd`%+^CjPCJjn`Tc%je zo{2M7D{&e26l1nUu%$dg3~mthj7180t#-e!#$*rx3jBE$AomGu34a=90}br>x0Zg25APS{-UMLT9AF&(6M2_w{stEh`8rEcO#DER)Qd zmeG|RklD^G4Ez%7fPpHjG2k!%6HmyY3!^HI)m+keVoU`QhlnCJixZ&gL8Bk#0xD+- zfE*3e)XJyYx|4FZ8gcszGa*@tuEeg+)EJy*fGw_^x-(yuWJ^{}%Dk!Crdqf0fj|6 zv&6O%r?SKs<1B-rqBX;yL0CLDf(SRA?5E74CgyH1xGzas`@s-*45$A|P4}rOSbFT+ z)R5x|zw-$nMx5WT%DA%-fw@#^(O}_A84Z6&@iDO$$arX;=PLSI}>55 zHm7bncZtW+bU;jgoVZ<))l_^!-YG_gkin&!on&Vb#K*M-oI8!kDz05vzFp~Iaj7Sm zEZ}C~R+A;tXN^HJhCsJ{k{4K`C;Shd@GZCw;6$P6^_c_IJs^1;JD??QMWxm8APq=) z17sCI8ks;VKPB>sJWL`Om@C=4i zVv~t4jAPwp94qn(!GUkdy(#{lwe19F9O-x2)O!(f15-%jz9qd0)FO3Z++pzqig8?O zOv9=-`=IL~w$+cp;u@F+?N%8G{OTWJlCS~fIvXs2ut%gpT3lg7$?IkyB}UW$p%E!R z2EcPf{w>HrA69a%RT*twXm}K;GF)%cjG5KBz=pZNo}949pgb;Cuwg_kGNL&dK>A*! z6AQbl6bZlFHfV~Ua)8KWS@2-_BRB8IY<}ku)r5p1}$TwNlZ7NrZM zUyc*Xn;1_ws7RC2{j8xuSf@eQD=0UpOAyc@<6zB%Wb}Ot6nNvBTe!;x2Qet?%BQ@cZ@%OANK<0r*ADgdr*~Buozso*_7I=@b)Tp|b z?cfGe7cFwR5*N|U67~1v?jdfxux$tVOuKBfskCTQH!IX-`)7{lgt`S`w;^&)+z`<# z;a@^4>a1m3G>p;*UBTtS-g-#F{y}hVVrJJ89Ga*wsaOH-jrE|(-68Q(SigU z4@+FxbW7kBy4EA&6d;*OHl<|+o-|@t#Riu!m~*&*MfbWazFXBG8G>EK5S2I)QDSu6 zqBvG#=H*TaE*y=;WlfgO7-uy&xIv^T88tMkAE#%31PU@AT)9rU487jN>y87i-}sPu z#-lrNNes|GrE1os6j~*Q63I;63IH52I(FMh`D4@_B>`$g2FkUa3$CO~VGq55hpVV@C4m?&f{_$!sIrbp(rU?;dw@Jfv}hYQzkC{QgOkej7x(?e_tyz zMm26l+JG%4{g08B?4++W*BxUSZi8n>Y`-t%dod&*ruSQ8=r5~Ct* z2XLzu_x?)!V4Hv}@x%)I0M-mnJi`-m;{Ih!dx9l>q6yYmmD5#CO}O5BsYy#gLnq{T z{~bq>w4gO<4O`Nb&B%c{@aV>Hy9D=9>Dsw{%j^Ye!*BjMK&Oc2yOCjjb7j5mZF#tA zElB>AN2qjPKJk?wyNHbYM#Xy-52`oMR&SoG-Z~f9mNU<=b>CP@iOTUvELpP9S&Wc? z8h>O0(qC}AjvJOr%@jY1KFc4;dDkp0@#FBvCKBz2$>igC-7)1@j!|HOJAJu?39ww+ z_*0^Fg<<24Fl?dA2OG@JZ@6fWVYx0YcIF| z##0UZc-?=^pE)w`-S|bt>g>D~{5IvzqHit{uq zm=nwpH5yqDSql=>#lHYPJ2CBt{l!g+{Q|M=u)wYemi>`z=jO%xX_D(MdZGtPW9IPJ zN^V{_v<*VeM;nOgd6Emu5KOTVop-4tifw;F9gTeSq< z+K1Sv6f9DSojk7TmsKO2L#~MZuLPRYtErB>Ex1Tjb++)=K&22|WW8jYSfQVC zh6<&FGyxSyMmXD$x6Bt}TU z`YV5q>ieWe z);|*bSNDFf;)ApApL@T-{62DP^R0^?Cg#@coEMr1;Lt7St
H@Dir<-Z#KK)rSF z!|?k*Vx9P}9=^5aL$qdCD;Lc84_ytWaSje5icA6K5Pt%3JlD83+;lJ`^!IloV2O8# z#~z5?)$R`MQYav%$n;|8v~+-OT*t&FNgKi6pcvaow>CKj=}-i!CzGnsRzo4}RmcNX zKA=}Gy;|wTD9dQPo<2o-6?s9sN*hB?(PtN4x)T?2jdPt>sO(jGMd)>bUKBXBv5R6b zEE1r+MLF7+>DA>PzF{cI_cGY~Ddor_GIlDBl%t>;TR7N8_=)$haj*KQ@|UO}qoZzx z(aSq|o_{Q?Ga)3jQ?zn4@=Ibnxpl(L6xcXC-Q}KJOR!Fcl%8A|&}rfj^W{spwD^ zc#%+1BjH6tc#*IoqhX`b5JfJ&jv6ANhDh?&4t_(%Nj+}JwzFjVICZ??v417MIs=qQ zrj2B#f04fO;Q-&9Jv-fsQ<)cL*EQuieBa-)NJ+jSz_(^QrZ-P()1ldhomAI)A0tAN zZwT_cs1qd97zL6Ui~>o1o1foAV?~mUE>iM%O@Kd;k+QAR&g{YWl7#2LjKq@9&^n0+p*P9M)s&en$r>%kd@_2A=8LB2K9k=>ltvZ2|! zEi{PM`NWokIMa2z9pk(+cU4d*F5Xnm?vcUAm^@r;1XwD;$j7TzsOr1KK9k4 zw_sJ?iInw()a;3J%k@O%{vyf%eZR;D`Nj<9gXHo0D!x7of+3j!rI6&e1o@4bcBwhD7e{upxb;25^M*js1dY`BO diff --git a/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc b/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc deleted file mode 100644 index 0f80db25d2fc872b45c0b7656967f41f76330ee3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14399 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 Date: Sun, 4 Jan 2026 05:17:28 +0000 Subject: [PATCH 7/8] feat: convert validation scripts to Python - Add scripts/validate/manifest.py for manifest validation - Add scripts/validate/php_syntax.py for PHP syntax checking - Add scripts/validate/xml_wellformed.py for XML validation - All Python validators maintain CLI compatibility with bash versions - Support both JSON output and verbose human-readable output - Update .gitignore to exclude Python cache files - Update Makefile with scaffolding commands Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- scripts/validate/manifest.py | 168 ++++++++++++++++++++++ scripts/validate/php_syntax.py | 218 +++++++++++++++++++++++++++++ scripts/validate/xml_wellformed.py | 206 +++++++++++++++++++++++++++ 3 files changed, 592 insertions(+) create mode 100755 scripts/validate/manifest.py create mode 100755 scripts/validate/php_syntax.py create mode 100755 scripts/validate/xml_wellformed.py diff --git a/scripts/validate/manifest.py b/scripts/validate/manifest.py new file mode 100755 index 0000000..c7a4351 --- /dev/null +++ b/scripts/validate/manifest.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Validate Joomla manifest files. + +Copyright (C) 2025 Moko Consulting + +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()) diff --git a/scripts/validate/php_syntax.py b/scripts/validate/php_syntax.py new file mode 100755 index 0000000..cd6251b --- /dev/null +++ b/scripts/validate/php_syntax.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Validate PHP syntax in files. + +Copyright (C) 2025 Moko Consulting + +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()) diff --git a/scripts/validate/xml_wellformed.py b/scripts/validate/xml_wellformed.py new file mode 100755 index 0000000..48adbc1 --- /dev/null +++ b/scripts/validate/xml_wellformed.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Validate XML well-formedness. + +Copyright (C) 2025 Moko Consulting + +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()) -- 2.49.1 From d1818ec85900ac384d2d92d2b7c039002fd9c780 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 05:19:06 +0000 Subject: [PATCH 8/8] feat: add Python packaging script for extensions - Add scripts/release/package_extension.py for creating distribution ZIPs - Auto-detects extension type, version, and manifest - Configurable exclusion patterns - Support for including/excluding docs and tests - Progress indicators during packaging - JSON output for automation - Tested and working (175 files, 477KB package) Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com> --- scripts/release/package_extension.py | 312 +++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100755 scripts/release/package_extension.py diff --git a/scripts/release/package_extension.py b/scripts/release/package_extension.py new file mode 100755 index 0000000..e1fb1fa --- /dev/null +++ b/scripts/release/package_extension.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +""" +Package Joomla extension as distributable ZIP. + +Copyright (C) 2025 Moko Consulting + +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()) -- 2.49.1