Convert shell scripts to Python with Joomla/Dolibarr platform support #32

Merged
Copilot merged 8 commits from copilot/convert-scripts-to-python into main 2026-01-04 08:29:37 +00:00
8 changed files with 448 additions and 56 deletions
Showing only changes of commit ef9bf28444 - Show all commits

View File

@@ -36,31 +36,36 @@ jobs:
run: |
git config --global core.autocrlf false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Verify script executability
run: |
chmod +x scripts/**/*.sh || true
chmod +x scripts/**/*.py || true
- name: Required validations
run: |
set -e
scripts/validate/manifest.sh
scripts/validate/xml_wellformed.sh
scripts/validate/workflows.sh
python3 scripts/validate/manifest.py
python3 scripts/validate/xml_wellformed.py
python3 scripts/validate/workflows.py
- name: Optional validations
run: |
set +e
scripts/validate/changelog.sh
scripts/validate/language_structure.sh
scripts/validate/license_headers.sh
scripts/validate/no_secrets.sh
scripts/validate/paths.sh
scripts/validate/php_syntax.sh
scripts/validate/tabs.sh
scripts/validate/version_alignment.sh
scripts/validate/version_hierarchy.sh
python3 scripts/validate/changelog.py || echo "changelog validation not yet converted"
python3 scripts/validate/language_structure.py || echo "language_structure validation not yet converted"
python3 scripts/validate/license_headers.py || echo "license_headers validation not yet converted"
python3 scripts/validate/no_secrets.py
python3 scripts/validate/paths.py
python3 scripts/validate/php_syntax.py
python3 scripts/validate/tabs.py
python3 scripts/validate/version_alignment.py || echo "version_alignment validation not yet converted"
python3 scripts/validate/version_hierarchy.py || echo "version_hierarchy validation not yet converted"
- name: CI summary
if: always()

View File

@@ -53,26 +53,31 @@ jobs:
exit 1
fi
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Run pre-deployment validations
run: |
chmod +x scripts/validate/*.sh
chmod +x scripts/validate/*.py
# Required validations
scripts/validate/manifest.sh
scripts/validate/xml_wellformed.sh
scripts/validate/php_syntax.sh
python3 scripts/validate/manifest.py
python3 scripts/validate/xml_wellformed.py
python3 scripts/validate/php_syntax.py
- name: Build deployment package
id: build
run: |
chmod +x scripts/release/package_extension.sh
chmod +x scripts/release/package_extension.py
VERSION="${{ inputs.version }}"
if [ -z "${VERSION}" ]; then
VERSION=$(grep -oP '<version>\K[^<]+' src/templates/templateDetails.xml | head -n 1)
fi
scripts/release/package_extension.sh dist "${VERSION}"
python3 scripts/release/package_extension.py dist "${VERSION}"
ZIP_FILE=$(ls dist/*.zip | head -n 1)
echo "package=${ZIP_FILE}" >> "$GITHUB_OUTPUT"

View File

@@ -103,21 +103,20 @@ jobs:
# Check if REF_NAME is main or matches version pattern
if [ "${REF_NAME}" = "main" ]; then
# Infer version from manifest when on main branch
SCRIPT_LIB_DIR="${GITHUB_WORKSPACE}/scripts/lib"
if [ ! -f "${SCRIPT_LIB_DIR}/joomla_manifest.sh" ]; then
echo "ERROR: Cannot find joomla_manifest.sh library" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
# Source the library functions
. "${SCRIPT_LIB_DIR}/joomla_manifest.sh"
# Find and extract version from manifest
MANIFEST="$(find_manifest "${GITHUB_WORKSPACE}/src")"
VERSION="$(get_manifest_version "${MANIFEST}")"
# Use Python library for cross-platform compatibility
VERSION=$(python3 -c "
import sys
sys.path.insert(0, '${GITHUB_WORKSPACE}/scripts/lib')
import extension_utils
ext_info = extension_utils.get_extension_info('${GITHUB_WORKSPACE}/src')
if ext_info:
print(ext_info.version)
else:
sys.exit(1)
")
if [ -z "${VERSION}" ]; then
echo "ERROR: Failed to extract version from manifest: ${MANIFEST}" >> "${GITHUB_STEP_SUMMARY}"
echo "ERROR: Failed to extract version from manifest" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
copilot-pull-request-reviewer[bot] commented 2026-01-04 08:35:23 +00:00 (Migrated from github.com)
Review

The import of extension_utils will fail because this module is not included in the PR. The GitHub Actions workflow will fail at this step since the Python code cannot be executed without the required module.

The import of `extension_utils` will fail because this module is not included in the PR. The GitHub Actions workflow will fail at this step since the Python code cannot be executed without the required module.
@@ -552,19 +551,19 @@ jobs:
set -euo pipefail
required_scripts=(
"scripts/validate/manifest.sh"
"scripts/validate/xml_wellformed.sh"
"scripts/validate/manifest.py"
"scripts/validate/xml_wellformed.py"
)
optional_scripts=(
"scripts/validate/changelog.sh"
"scripts/validate/language_structure.sh"
"scripts/validate/license_headers.sh"
"scripts/validate/no_secrets.sh"
"scripts/validate/paths.sh"
"scripts/validate/php_syntax.sh"
"scripts/validate/tabs.sh"
"scripts/validate/version_alignment.sh"
"scripts/validate/changelog.py"
"scripts/validate/language_structure.py"
"scripts/validate/license_headers.py"
"scripts/validate/no_secrets.py"
"scripts/validate/paths.py"
"scripts/validate/php_syntax.py"
"scripts/validate/tabs.py"
"scripts/validate/version_alignment.py"
)
missing=()
@@ -596,7 +595,7 @@ jobs:
for s in "${required_scripts[@]}" "${optional_scripts[@]}"; do
if [ -f "${s}" ]; then
chmod +x "${s}"
"${s}" >> "${GITHUB_STEP_SUMMARY}"
python3 "${s}" >> "${GITHUB_STEP_SUMMARY}"
ran+=("${s}")
else
skipped+=("${s}")

View File

@@ -34,14 +34,14 @@ install:
## validate: Run all validation scripts
validate:
@echo "Running validation scripts..."
@./scripts/run/validate_all.sh
@python3 ./scripts/run/validate_all.py
## 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
@python3 ./scripts/validate/manifest.py
@python3 ./scripts/validate/xml_wellformed.py
@python3 ./scripts/validate/workflows.py
@echo "✓ Required validations passed"
## test: Run all tests
@@ -96,18 +96,18 @@ phpcompat:
## package: Create distribution package
package:
@echo "Creating distribution package..."
@./scripts/release/package_extension.sh dist $(VERSION)
@python3 ./scripts/release/package_extension.py 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
@python3 ./scripts/run/smoke_test.py
## script-health: Check script health
script-health:
@echo "Checking script health..."
@./scripts/run/script_health.sh
@python3 ./scripts/run/script_health.py
## version-check: Display current version information
version-check:
@@ -119,7 +119,7 @@ version-check:
## fix-permissions: Fix script executable permissions
fix-permissions:
@echo "Fixing script permissions..."
@find scripts -type f -name "*.sh" -exec chmod +x {} \;
@find scripts -type f -name "*.py" -exec chmod +x {} \;
@echo "✓ Permissions fixed"
## clean: Remove generated files and caches
@@ -174,13 +174,10 @@ watch:
## list-scripts: List all available scripts
list-scripts:
@echo "Available validation scripts:"
@find scripts/validate -type f -name "*.sh" -exec basename {} \; | sort
@find scripts/validate -type f -name "*.py" -exec basename {} \; | sort
@echo ""
@echo "Available fix scripts:"
@find scripts/fix -type f -name "*.sh" -exec basename {} \; | sort
@echo ""
@echo "Available run scripts (bash):"
@find scripts/run -type f -name "*.sh" -exec basename {} \; | sort
@find scripts/fix -type f -name "*.py" -exec basename {} \; | sort
@echo ""
@echo "Available run scripts (python):"
@find scripts/run -type f -name "*.py" -exec basename {} \; | sort

0
scripts/validate/no_secrets.py Normal file → Executable file
View File

169
scripts/validate/paths.py Executable file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Detect Windows-style path separators (backslashes).
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.Validate
INGROUP: Path.Normalization
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
PATH: /scripts/validate/paths.py
VERSION: 01.00.00
BRIEF: Detect Windows-style path separators (backslashes)
NOTE: Ensures cross-platform path compatibility
"""
import mimetypes
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple, Dict
# 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 get_tracked_files() -> List[str]:
"""
Get list of files tracked by git.
Returns:
List of file paths
"""
try:
result = common.run_command(
["git", "ls-files", "-z"],
capture_output=True,
check=True
)
files = [f for f in result.stdout.split('\0') if f.strip()]
return files
except subprocess.CalledProcessError:
return []
def is_binary_file(filepath: str) -> bool:
"""
Check if a file is likely binary.
Args:
filepath: Path to file
Returns:
True if likely binary
"""
# Check mime type
mime_type, _ = mimetypes.guess_type(filepath)
if mime_type and mime_type.startswith(('application/', 'audio/', 'image/', 'video/')):
return True
# Check for null bytes (heuristic for binary files)
try:
with open(filepath, 'rb') as f:
chunk = f.read(1024)
if b'\x00' in chunk:
return True
except Exception:
return True
return False
def find_backslashes_in_file(filepath: str) -> List[Tuple[int, str]]:
"""
Find lines with backslashes in a file.
Args:
filepath: Path to file
Returns:
List of (line_number, line_content) tuples
"""
backslashes = []
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for line_num, line in enumerate(f, 1):
if '\\' in line:
backslashes.append((line_num, line.rstrip()))
except Exception as e:
common.log_warn(f"Could not read {filepath}: {e}")
return backslashes
def main() -> int:
"""Main entry point."""
tracked_files = get_tracked_files()
if not tracked_files:
print("No files to check")
return 0
hits: Dict[str, List[Tuple[int, str]]] = {}
for filepath in tracked_files:
# Skip binary files
if is_binary_file(filepath):
continue
# Find backslashes
backslashes = find_backslashes_in_file(filepath)
if backslashes:
hits[filepath] = backslashes
if hits:
print("ERROR: Windows-style path literals detected", file=sys.stderr)
print("", file=sys.stderr)
print(f"Found backslashes in {len(hits)} file(s):", file=sys.stderr)
for filepath, lines in hits.items():
print("", file=sys.stderr)
print(f" File: {filepath}", file=sys.stderr)
print(" Lines with backslashes:", file=sys.stderr)
# Show first 5 lines
for line_num, line_content in lines[:5]:
print(f" {line_num}: {line_content[:80]}", file=sys.stderr)
if len(lines) > 5:
print(f" ... and {len(lines) - 5} more", file=sys.stderr)
print("", file=sys.stderr)
print("To fix:", file=sys.stderr)
print(" 1. Run: python3 scripts/fix/paths.py", file=sys.stderr)
print(" 2. Or manually replace backslashes (\\) with forward slashes (/)", file=sys.stderr)
print(" 3. Ensure paths use POSIX separators for cross-platform compatibility", file=sys.stderr)
print("", file=sys.stderr)
return 2
print("paths: ok")
return 0
if __name__ == "__main__":
sys.exit(main())

0
scripts/validate/tabs.py Normal file → Executable file
View File

217
scripts/validate/workflows.py Executable file
View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""
Validate GitHub Actions workflow files.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (./LICENSE.md).
FILE INFORMATION
DEFGROUP: Script.Validate
INGROUP: CI.Validation
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
PATH: /scripts/validate/workflows.py
VERSION: 01.00.00
BRIEF: Validate GitHub Actions workflow files
NOTE: Checks YAML syntax, structure, and best practices
"""
import sys
from pathlib import Path
from typing import List, Tuple
github-code-quality[bot] commented 2026-01-04 08:19:55 +00:00 (Migrated from github.com)
Review

Unused import

Import of 'List' is not used.

Import of 'Tuple' is not used.


To fix an unused-import problem, remove the imported names that are not referenced anywhere in the file. This eliminates the static analysis warning without altering runtime behavior.

In this case, the import from typing import List, Tuple on line 36 is not used anywhere in the provided code. The best fix is to delete this line entirely. No other changes are needed: no new imports, no new functions, and no call-site updates. This change should be applied in scripts/validate/workflows.py at the import section near the top of the file, specifically removing line 36 and leaving the import sys and from pathlib import Path lines intact.

## Unused import Import of 'List' is not used.<br> Import of 'Tuple' is not used. --- To fix an unused-import problem, remove the imported names that are not referenced anywhere in the file. This eliminates the static analysis warning without altering runtime behavior.</p> <p>In this case, the import <code>from typing import List, Tuple</code> on line 36 is not used anywhere in the provided code. The best fix is to delete this line entirely. No other changes are needed: no new imports, no new functions, and no call-site updates. This change should be applied in <code>scripts/validate/workflows.py</code> at the import section near the top of the file, specifically removing line 36 and leaving the <code>import sys</code> and <code>from pathlib import Path</code> lines intact.
copilot-pull-request-reviewer[bot] commented 2026-01-04 08:35:25 +00:00 (Migrated from github.com)
Review

Import of 'List' is not used.
Import of 'Tuple' is not used.

Import of 'List' is not used. Import of 'Tuple' is not used.
# 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 validate_yaml_syntax(filepath: Path) -> bool:
"""
Validate YAML syntax of a workflow file.
Args:
filepath: Path to workflow file
Returns:
True if valid
"""
try:
import yaml
except ImportError:
common.log_warn("PyYAML module not installed. Install with: pip3 install pyyaml")
return True # Skip validation if yaml not available
try:
with open(filepath, 'r', encoding='utf-8') as f:
yaml.safe_load(f)
print(f"✓ Valid YAML: {filepath.name}")
return True
except yaml.YAMLError as e:
print(f"✗ YAML Error in {filepath.name}: {e}", file=sys.stderr)
return False
except Exception as e:
print(f"✗ Error reading {filepath.name}: {e}", file=sys.stderr)
return False
def check_no_tabs(filepath: Path) -> bool:
"""
Check that file contains no tab characters.
Args:
filepath: Path to file
Returns:
True if no tabs found
"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if '\t' in content:
common.log_error(f"✗ File contains tab characters: {filepath.name}")
return False
except Exception as e:
common.log_warn(f"Could not read {filepath}: {e}")
return False
return True
def check_workflow_structure(filepath: Path) -> bool:
"""
Check workflow file structure for required keys.
Args:
filepath: Path to workflow file
Returns:
True if structure is valid
"""
errors = 0
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Check for required top-level keys
if 'name:' not in content and not content.startswith('name:'):
common.log_warn(f"Missing 'name:' in {filepath.name}")
if 'on:' not in content and not content.startswith('on:'):
common.log_error(f"✗ Missing 'on:' trigger in {filepath.name}")
errors += 1
if 'jobs:' not in content and not content.startswith('jobs:'):
common.log_error(f"✗ Missing 'jobs:' in {filepath.name}")
errors += 1
except Exception as e:
common.log_error(f"Error reading {filepath}: {e}")
return False
return errors == 0
def validate_workflow_file(filepath: Path) -> bool:
"""
Validate a single workflow file.
Args:
filepath: Path to workflow file
Returns:
True if valid
"""
common.log_info(f"Validating: {filepath.name}")
errors = 0
# Check YAML syntax
if not validate_yaml_syntax(filepath):
errors += 1
# Check for tabs
if not check_no_tabs(filepath):
errors += 1
# Check structure
if not check_workflow_structure(filepath):
errors += 1
if errors == 0:
common.log_info(f"{filepath.name} passed all checks")
return True
else:
common.log_error(f"{filepath.name} failed {errors} check(s)")
return False
def main() -> int:
"""Main entry point."""
common.log_info("GitHub Actions Workflow Validation")
common.log_info("===================================")
print()
workflows_dir = Path(".github/workflows")
if not workflows_dir.is_dir():
common.log_error(f"Workflows directory not found: {workflows_dir}")
return 1
# Find all workflow files
workflow_files = []
for pattern in ["*.yml", "*.yaml"]:
workflow_files.extend(workflows_dir.glob(pattern))
if not workflow_files:
common.log_warn("No workflow files found")
return 0
total = len(workflow_files)
passed = 0
failed = 0
for workflow in workflow_files:
if validate_workflow_file(workflow):
passed += 1
else:
failed += 1
print()
common.log_info("===================================")
common.log_info("Summary:")
common.log_info(f" Total workflows: {total}")
common.log_info(f" Passed: {passed}")
common.log_info(f" Failed: {failed}")
common.log_info("===================================")
if failed > 0:
common.log_error("Workflow validation failed")
return 1
common.log_info("All workflows validated successfully")
return 0
if __name__ == "__main__":
sys.exit(main())