Improve Joomla development workflows and convert scripts to Python #31

Merged
Copilot merged 8 commits from copilot/improve-joomla-development-workflow into main 2026-01-04 05:34:19 +00:00
Showing only changes of commit d1818ec859 - Show all commits

View File

@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Package Joomla extension as distributable ZIP.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (./LICENSE.md).
FILE INFORMATION
DEFGROUP: Script.Release
INGROUP: Extension.Packaging
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
PATH: /scripts/release/package_extension.py
VERSION: 01.00.00
BRIEF: Package Joomla extension as distributable ZIP
USAGE: ./scripts/release/package_extension.py [output_dir] [version]
"""
import argparse
import os
copilot-pull-request-reviewer[bot] commented 2026-01-04 05:38:50 +00:00 (Migrated from github.com)
Review

Import of 'os' is not used.


Import of 'os' is not used. ```suggestion ```
import shutil
copilot-pull-request-reviewer[bot] commented 2026-01-04 05:38:51 +00:00 (Migrated from github.com)
Review

Import of 'shutil' is not used.


Import of 'shutil' is not used. ```suggestion ```
import sys
import zipfile
from datetime import datetime
from pathlib import Path
from typing import List, Set
copilot-pull-request-reviewer[bot] commented 2026-01-04 05:38:51 +00:00 (Migrated from github.com)
Review

Import of 'List' is not used.

from typing import Set
Import of 'List' is not used. ```suggestion from typing import 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
copilot-pull-request-reviewer[bot] commented 2026-01-04 05:38:48 +00:00 (Migrated from github.com)
Review

The import statement uses fnmatch inside the function instead of at the module level. This violates PEP 8 import conventions and can impact performance when the function is called repeatedly. Import statements should be placed at the top of the file with other imports.

The import statement uses `fnmatch` inside the function instead of at the module level. This violates PEP 8 import conventions and can impact performance when the function is called repeatedly. Import statements should be placed at the top of the file with other imports.
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")
copilot-pull-request-reviewer[bot] commented 2026-01-04 05:38:49 +00:00 (Migrated from github.com)
Review

The variable 'timestamp' is assigned on line 165 but never used in the code. This appears to be unused code that should either be removed or incorporated into the ZIP filename if timestamping is desired.


The variable 'timestamp' is assigned on line 165 but never used in the code. This appears to be unused code that should either be removed or incorporated into the ZIP filename if timestamping is desired. ```suggestion ```
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(
copilot-pull-request-reviewer[bot] commented 2026-01-04 05:38:50 +00:00 (Migrated from github.com)
Review

Variable zip_path is not used.

        create_package(
Variable zip_path is not used. ```suggestion 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())