Convert shell scripts to Python with Joomla/Dolibarr platform support #32
31
.github/workflows/ci.yml
vendored
@@ -36,31 +36,36 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config --global core.autocrlf false
|
git config --global core.autocrlf false
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Verify script executability
|
- name: Verify script executability
|
||||||
run: |
|
run: |
|
||||||
chmod +x scripts/**/*.sh || true
|
chmod +x scripts/**/*.py || true
|
||||||
|
|
||||||
- name: Required validations
|
- name: Required validations
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
scripts/validate/manifest.sh
|
python3 scripts/validate/manifest.py
|
||||||
scripts/validate/xml_wellformed.sh
|
python3 scripts/validate/xml_wellformed.py
|
||||||
scripts/validate/workflows.sh
|
python3 scripts/validate/workflows.py
|
||||||
|
|
||||||
- name: Optional validations
|
- name: Optional validations
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
scripts/validate/changelog.sh
|
python3 scripts/validate/changelog.py || echo "changelog validation not yet converted"
|
||||||
scripts/validate/language_structure.sh
|
python3 scripts/validate/language_structure.py || echo "language_structure validation not yet converted"
|
||||||
scripts/validate/license_headers.sh
|
python3 scripts/validate/license_headers.py || echo "license_headers validation not yet converted"
|
||||||
scripts/validate/no_secrets.sh
|
python3 scripts/validate/no_secrets.py
|
||||||
scripts/validate/paths.sh
|
python3 scripts/validate/paths.py
|
||||||
scripts/validate/php_syntax.sh
|
python3 scripts/validate/php_syntax.py
|
||||||
scripts/validate/tabs.sh
|
python3 scripts/validate/tabs.py
|
||||||
scripts/validate/version_alignment.sh
|
python3 scripts/validate/version_alignment.py || echo "version_alignment validation not yet converted"
|
||||||
scripts/validate/version_hierarchy.sh
|
python3 scripts/validate/version_hierarchy.py || echo "version_hierarchy validation not yet converted"
|
||||||
|
|
||||||
- name: CI summary
|
- name: CI summary
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
17
.github/workflows/deploy_staging.yml
vendored
@@ -53,26 +53,31 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Run pre-deployment validations
|
- name: Run pre-deployment validations
|
||||||
run: |
|
run: |
|
||||||
chmod +x scripts/validate/*.sh
|
chmod +x scripts/validate/*.py
|
||||||
|
|
||||||
# Required validations
|
# Required validations
|
||||||
scripts/validate/manifest.sh
|
python3 scripts/validate/manifest.py
|
||||||
scripts/validate/xml_wellformed.sh
|
python3 scripts/validate/xml_wellformed.py
|
||||||
scripts/validate/php_syntax.sh
|
python3 scripts/validate/php_syntax.py
|
||||||
|
|
||||||
- name: Build deployment package
|
- name: Build deployment package
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
chmod +x scripts/release/package_extension.sh
|
chmod +x scripts/release/package_extension.py
|
||||||
|
|
||||||
VERSION="${{ inputs.version }}"
|
VERSION="${{ inputs.version }}"
|
||||||
if [ -z "${VERSION}" ]; then
|
if [ -z "${VERSION}" ]; then
|
||||||
VERSION=$(grep -oP '<version>\K[^<]+' src/templates/templateDetails.xml | head -n 1)
|
VERSION=$(grep -oP '<version>\K[^<]+' src/templates/templateDetails.xml | head -n 1)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
scripts/release/package_extension.sh dist "${VERSION}"
|
python3 scripts/release/package_extension.py dist "${VERSION}"
|
||||||
|
|
||||||
ZIP_FILE=$(ls dist/*.zip | head -n 1)
|
ZIP_FILE=$(ls dist/*.zip | head -n 1)
|
||||||
echo "package=${ZIP_FILE}" >> "$GITHUB_OUTPUT"
|
echo "package=${ZIP_FILE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
47
.github/workflows/release_pipeline.yml
vendored
@@ -103,21 +103,20 @@ jobs:
|
|||||||
# Check if REF_NAME is main or matches version pattern
|
# Check if REF_NAME is main or matches version pattern
|
||||||
if [ "${REF_NAME}" = "main" ]; then
|
if [ "${REF_NAME}" = "main" ]; then
|
||||||
# Infer version from manifest when on main branch
|
# Infer version from manifest when on main branch
|
||||||
SCRIPT_LIB_DIR="${GITHUB_WORKSPACE}/scripts/lib"
|
# Use Python library for cross-platform compatibility
|
||||||
if [ ! -f "${SCRIPT_LIB_DIR}/joomla_manifest.sh" ]; then
|
VERSION=$(python3 -c "
|
||||||
echo "ERROR: Cannot find joomla_manifest.sh library" >> "${GITHUB_STEP_SUMMARY}"
|
import sys
|
||||||
exit 1
|
sys.path.insert(0, '${GITHUB_WORKSPACE}/scripts/lib')
|
||||||
fi
|
import extension_utils
|
||||||
|
ext_info = extension_utils.get_extension_info('${GITHUB_WORKSPACE}/src')
|
||||||
# Source the library functions
|
if ext_info:
|
||||||
. "${SCRIPT_LIB_DIR}/joomla_manifest.sh"
|
print(ext_info.version)
|
||||||
|
else:
|
||||||
# Find and extract version from manifest
|
sys.exit(1)
|
||||||
MANIFEST="$(find_manifest "${GITHUB_WORKSPACE}/src")"
|
")
|
||||||
VERSION="$(get_manifest_version "${MANIFEST}")"
|
|
||||||
|
|
||||||
if [ -z "${VERSION}" ]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|||||||
@@ -552,19 +551,19 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
required_scripts=(
|
required_scripts=(
|
||||||
"scripts/validate/manifest.sh"
|
"scripts/validate/manifest.py"
|
||||||
"scripts/validate/xml_wellformed.sh"
|
"scripts/validate/xml_wellformed.py"
|
||||||
)
|
)
|
||||||
|
|
||||||
optional_scripts=(
|
optional_scripts=(
|
||||||
"scripts/validate/changelog.sh"
|
"scripts/validate/changelog.py"
|
||||||
"scripts/validate/language_structure.sh"
|
"scripts/validate/language_structure.py"
|
||||||
"scripts/validate/license_headers.sh"
|
"scripts/validate/license_headers.py"
|
||||||
"scripts/validate/no_secrets.sh"
|
"scripts/validate/no_secrets.py"
|
||||||
"scripts/validate/paths.sh"
|
"scripts/validate/paths.py"
|
||||||
"scripts/validate/php_syntax.sh"
|
"scripts/validate/php_syntax.py"
|
||||||
"scripts/validate/tabs.sh"
|
"scripts/validate/tabs.py"
|
||||||
"scripts/validate/version_alignment.sh"
|
"scripts/validate/version_alignment.py"
|
||||||
)
|
)
|
||||||
|
|
||||||
missing=()
|
missing=()
|
||||||
@@ -596,7 +595,7 @@ jobs:
|
|||||||
for s in "${required_scripts[@]}" "${optional_scripts[@]}"; do
|
for s in "${required_scripts[@]}" "${optional_scripts[@]}"; do
|
||||||
if [ -f "${s}" ]; then
|
if [ -f "${s}" ]; then
|
||||||
chmod +x "${s}"
|
chmod +x "${s}"
|
||||||
"${s}" >> "${GITHUB_STEP_SUMMARY}"
|
python3 "${s}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
ran+=("${s}")
|
ran+=("${s}")
|
||||||
else
|
else
|
||||||
skipped+=("${s}")
|
skipped+=("${s}")
|
||||||
|
|||||||
23
Makefile
@@ -34,14 +34,14 @@ install:
|
|||||||
## validate: Run all validation scripts
|
## validate: Run all validation scripts
|
||||||
validate:
|
validate:
|
||||||
@echo "Running validation scripts..."
|
@echo "Running validation scripts..."
|
||||||
@./scripts/run/validate_all.sh
|
@python3 ./scripts/run/validate_all.py
|
||||||
|
|
||||||
## validate-required: Run only required validation scripts
|
## validate-required: Run only required validation scripts
|
||||||
validate-required:
|
validate-required:
|
||||||
@echo "Running required validations..."
|
@echo "Running required validations..."
|
||||||
@./scripts/validate/manifest.sh
|
@python3 ./scripts/validate/manifest.py
|
||||||
@./scripts/validate/xml_wellformed.sh
|
@python3 ./scripts/validate/xml_wellformed.py
|
||||||
@./scripts/validate/workflows.sh
|
@python3 ./scripts/validate/workflows.py
|
||||||
@echo "✓ Required validations passed"
|
@echo "✓ Required validations passed"
|
||||||
|
|
||||||
## test: Run all tests
|
## test: Run all tests
|
||||||
@@ -96,18 +96,18 @@ phpcompat:
|
|||||||
## package: Create distribution package
|
## package: Create distribution package
|
||||||
package:
|
package:
|
||||||
@echo "Creating distribution 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"
|
@echo "✓ Package created: dist/moko-cassiopeia-$(VERSION)-*.zip"
|
||||||
|
|
||||||
## smoke-test: Run smoke tests
|
## smoke-test: Run smoke tests
|
||||||
smoke-test:
|
smoke-test:
|
||||||
@echo "Running smoke tests..."
|
@echo "Running smoke tests..."
|
||||||
@./scripts/run/smoke_test.sh
|
@python3 ./scripts/run/smoke_test.py
|
||||||
|
|
||||||
## script-health: Check script health
|
## script-health: Check script health
|
||||||
script-health:
|
script-health:
|
||||||
@echo "Checking 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: Display current version information
|
||||||
version-check:
|
version-check:
|
||||||
@@ -119,7 +119,7 @@ version-check:
|
|||||||
## fix-permissions: Fix script executable permissions
|
## fix-permissions: Fix script executable permissions
|
||||||
fix-permissions:
|
fix-permissions:
|
||||||
@echo "Fixing script 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"
|
@echo "✓ Permissions fixed"
|
||||||
|
|
||||||
## clean: Remove generated files and caches
|
## clean: Remove generated files and caches
|
||||||
@@ -174,13 +174,10 @@ watch:
|
|||||||
## list-scripts: List all available scripts
|
## list-scripts: List all available scripts
|
||||||
list-scripts:
|
list-scripts:
|
||||||
@echo "Available validation 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 ""
|
||||||
@echo "Available fix scripts:"
|
@echo "Available fix scripts:"
|
||||||
@find scripts/fix -type f -name "*.sh" -exec basename {} \; | sort
|
@find scripts/fix -type f -name "*.py" -exec basename {} \; | sort
|
||||||
@echo ""
|
|
||||||
@echo "Available run scripts (bash):"
|
|
||||||
@find scripts/run -type f -name "*.sh" -exec basename {} \; | sort
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Available run scripts (python):"
|
@echo "Available run scripts (python):"
|
||||||
@find scripts/run -type f -name "*.py" -exec basename {} \; | sort
|
@find scripts/run -type f -name "*.py" -exec basename {} \; | sort
|
||||||
|
|||||||
12
README.md
@@ -152,7 +152,7 @@ make quality
|
|||||||
make package
|
make package
|
||||||
|
|
||||||
# Install Git hooks (optional but recommended)
|
# Install Git hooks (optional but recommended)
|
||||||
./scripts/git/install-hooks.sh
|
python3 ./scripts/git/install-hooks.py
|
||||||
```
|
```
|
||||||
|
The code references The code references `scripts/git/install-hooks.py` but this file is shown as deleted (.sh version) without a Python replacement being added in the PR. This will break the installation process.
```suggestion
./scripts/git/install-hooks.sh
```
|
|||||||
|
|
||||||
**New to the project?** See [Quick Start Guide](./docs/QUICK_START.md) for a 5-minute walkthrough.
|
**New to the project?** See [Quick Start Guide](./docs/QUICK_START.md) for a 5-minute walkthrough.
|
||||||
@@ -168,15 +168,23 @@ make package
|
|||||||
### Available Tools
|
### Available Tools
|
||||||
|
|
||||||
- **Makefile**: Run `make help` to see all available commands
|
- **Makefile**: Run `make help` to see all available commands
|
||||||
|
- **Python Scripts**: All automation scripts are now Python-based for cross-platform compatibility
|
||||||
- **Pre-commit Hooks**: Automatic validation before commits
|
- **Pre-commit Hooks**: Automatic validation before commits
|
||||||
- **VS Code Tasks**: Pre-configured development tasks
|
- **VS Code Tasks**: Pre-configured development tasks
|
||||||
- **GitHub Actions**: Automated CI/CD pipelines
|
- **GitHub Actions**: Automated CI/CD pipelines
|
||||||
|
|
||||||
|
### Cross-Platform Support
|
||||||
|
|
||||||
|
All scripts are now written in Python for maximum cross-platform compatibility:
|
||||||
|
- **Joomla Extension Support**: Full support for Joomla 4.x and 5.x templates, components, modules, and plugins
|
||||||
|
- **Dolibarr Module Support**: Automatic detection and packaging of Dolibarr modules
|
||||||
|
- **Platform Detection**: Scripts automatically detect whether you're working with Joomla or Dolibarr extensions
|
||||||
|
|
||||||
### Joomla Development Workflows
|
### Joomla Development Workflows
|
||||||
|
|
||||||
Comprehensive Joomla-aware development tools and workflows are available:
|
Comprehensive Joomla-aware development tools and workflows are available:
|
||||||
|
|
||||||
- **Extension Packaging** - Create distributable ZIP packages
|
- **Extension Packaging** - Create distributable ZIP packages for Joomla or Dolibarr
|
||||||
- **PHP Quality Checks** - PHPStan and PHP_CodeSniffer with Joomla standards
|
- **PHP Quality Checks** - PHPStan and PHP_CodeSniffer with Joomla standards
|
||||||
- **Automated Testing** - Codeception framework with multiple Joomla versions
|
- **Automated Testing** - Codeception framework with multiple Joomla versions
|
||||||
- **CI/CD Pipelines** - GitHub Actions with caching for faster builds
|
- **CI/CD Pipelines** - GitHub Actions with caching for faster builds
|
||||||
|
|||||||
@@ -1,522 +1,75 @@
|
|||||||
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
# Scripts Documentation
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
All automation scripts for the moko-cassiopeia project are written in Python for cross-platform compatibility and support both Joomla and Dolibarr extensions.
|
||||||
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
## Quick Reference
|
||||||
|
|
||||||
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. If not, see https://www.gnu.org/licenses/ .
|
|
||||||
|
|
||||||
FILE INFORMATION
|
|
||||||
DEFGROUP: Moko-Cassiopeia.Documentation
|
|
||||||
INGROUP: Scripts.Documentation
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
FILE: ./scripts/README.md
|
|
||||||
VERSION: 01.00.00
|
|
||||||
BRIEF: Documentation for repository automation scripts
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Scripts Directory
|
|
||||||
|
|
||||||
This directory contains automation scripts for repository management, validation,
|
|
||||||
and release processes for the Moko-Cassiopeia Joomla template.
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
scripts/
|
|
||||||
├── fix/ # Scripts that automatically fix common issues
|
|
||||||
├── lib/ # Shared library functions
|
|
||||||
├── release/ # Release automation scripts
|
|
||||||
├── run/ # Execution and testing scripts
|
|
||||||
└── validate/ # Validation and linting scripts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Library Files (`lib/`)
|
|
||||||
|
|
||||||
### `common.sh`
|
|
||||||
Core utilities used by all scripts:
|
|
||||||
- Environment normalization
|
|
||||||
- Logging functions (`log_info`, `log_warn`, `log_error`, `die`)
|
|
||||||
- Validation helpers (`assert_file_exists`, `assert_dir_exists`)
|
|
||||||
- JSON utilities (`json_escape`, `json_output`)
|
|
||||||
- Path helpers (`script_root`, `normalize_path`)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
. "$(dirname "$0")/../lib/common.sh"
|
|
||||||
log_info "Starting process..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### `joomla_manifest.sh`
|
|
||||||
Joomla manifest parsing utilities:
|
|
||||||
- `find_manifest <src_dir>` - Find primary Joomla manifest XML
|
|
||||||
- `get_manifest_version <manifest>` - Extract version from manifest
|
|
||||||
- `get_manifest_name <manifest>` - Extract extension name
|
|
||||||
- `get_manifest_type <manifest>` - Extract extension type
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
. "${SCRIPT_DIR}/lib/joomla_manifest.sh"
|
|
||||||
MANIFEST="$(find_manifest src)"
|
|
||||||
VERSION="$(get_manifest_version "${MANIFEST}")"
|
|
||||||
```
|
|
||||||
|
|
||||||
### `logging.sh`
|
|
||||||
Enhanced logging with structured output:
|
|
||||||
- Colored output support (when in terminal)
|
|
||||||
- Log levels: `log_debug`, `log_success`, `log_step`
|
|
||||||
- Structured logging: `log_kv`, `log_item`, `log_section`
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
. "${SCRIPT_DIR}/lib/logging.sh"
|
|
||||||
log_section "Starting validation"
|
|
||||||
log_kv "Version" "${VERSION}"
|
|
||||||
log_success "All checks passed"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validation Scripts (`validate/`)
|
|
||||||
|
|
||||||
These scripts validate repository structure, code quality, and compliance.
|
|
||||||
They are typically run in CI pipelines.
|
|
||||||
|
|
||||||
### `manifest.sh`
|
|
||||||
Validates Joomla manifest XML structure and required fields.
|
|
||||||
|
|
||||||
### `version_alignment.sh`
|
|
||||||
Checks that manifest version is documented in CHANGELOG.md.
|
|
||||||
|
|
||||||
### `php_syntax.sh`
|
|
||||||
Validates PHP syntax using `php -l` on all PHP files.
|
|
||||||
|
|
||||||
### `xml_wellformed.sh`
|
|
||||||
Validates that all XML files are well-formed.
|
|
||||||
|
|
||||||
### `tabs.sh`
|
|
||||||
Detects tab characters in source files (enforces spaces).
|
|
||||||
|
|
||||||
### `paths.sh`
|
|
||||||
Detects Windows-style path separators (backslashes).
|
|
||||||
|
|
||||||
### `no_secrets.sh`
|
|
||||||
Scans for accidentally committed secrets and credentials.
|
|
||||||
|
|
||||||
### `license_headers.sh`
|
|
||||||
Checks that source files contain SPDX license identifiers.
|
|
||||||
|
|
||||||
### `language_structure.sh`
|
|
||||||
Validates Joomla language directory structure and INI files.
|
|
||||||
|
|
||||||
### `changelog.sh`
|
|
||||||
Validates CHANGELOG.md structure and version entries.
|
|
||||||
|
|
||||||
### `version_hierarchy.sh`
|
|
||||||
Validates version hierarchy across branch prefixes:
|
|
||||||
- Checks for version conflicts across dev/, rc/, and version/ branches
|
|
||||||
- Ensures no version exists in multiple priority levels simultaneously
|
|
||||||
- Reports any violations of the hierarchy rules
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/validate/version_hierarchy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### `workflows.sh`
|
|
||||||
Validates GitHub Actions workflow files:
|
|
||||||
- Checks YAML syntax using Python's yaml module
|
|
||||||
- Ensures no tab characters are present
|
|
||||||
- Validates required workflow structure (name, on, jobs)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/validate/workflows.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fix Scripts (`fix/`)
|
|
||||||
|
|
||||||
These scripts automatically fix common issues detected by validation scripts.
|
|
||||||
|
|
||||||
### `tabs.sh`
|
|
||||||
Replaces tab characters with spaces in YAML files.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/fix/tabs.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### `paths.sh`
|
|
||||||
Normalizes Windows-style path separators to forward slashes.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/fix/paths.sh [directory]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `versions.sh`
|
|
||||||
Updates version numbers across repository files.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/fix/versions.sh <VERSION>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```bash
|
|
||||||
./scripts/fix/versions.sh 3.5.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Updates:
|
|
||||||
- Manifest XML `<version>` tag
|
|
||||||
- `package.json` version field
|
|
||||||
- Version references in README.md
|
|
||||||
|
|
||||||
## Release Scripts (`release/`)
|
|
||||||
|
|
||||||
Scripts for release automation and version management.
|
|
||||||
|
|
||||||
### `update_changelog.sh`
|
|
||||||
Inserts a new version entry in CHANGELOG.md.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/release/update_changelog.sh <VERSION>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```bash
|
|
||||||
./scripts/release/update_changelog.sh 03.05.00
|
|
||||||
```
|
|
||||||
|
|
||||||
### `update_dates.sh`
|
|
||||||
Normalizes release dates across manifests and CHANGELOG.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/release/update_dates.sh <YYYY-MM-DD> <VERSION>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```bash
|
|
||||||
./scripts/release/update_dates.sh 2025-01-15 03.05.00
|
|
||||||
```
|
|
||||||
|
|
||||||
### `package_extension.sh`
|
|
||||||
Package the Joomla template as a distributable ZIP file.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/release/package_extension.sh [output_dir] [version]
|
|
||||||
```
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- `output_dir` (optional): Output directory for ZIP file (default: `dist`)
|
|
||||||
- `version` (optional): Version string (default: extracted from manifest)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```bash
|
|
||||||
# Package with defaults (dist directory, auto-detect version)
|
|
||||||
./scripts/release/package_extension.sh
|
|
||||||
|
|
||||||
# Package to specific directory with version
|
|
||||||
./scripts/release/package_extension.sh /tmp/packages 3.5.0
|
|
||||||
|
|
||||||
# Package to dist with specific version
|
|
||||||
./scripts/release/package_extension.sh dist 3.5.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Automatically detects extension type from manifest
|
|
||||||
- Excludes development files (node_modules, vendor, tests, etc.)
|
|
||||||
- Validates manifest before packaging
|
|
||||||
- Creates properly structured Joomla installation package
|
|
||||||
- Outputs JSON status for automation
|
|
||||||
|
|
||||||
## Run Scripts (`run/`)
|
|
||||||
|
|
||||||
Execution and testing scripts.
|
|
||||||
|
|
||||||
### `smoke_test.sh`
|
|
||||||
Runs basic smoke tests to verify repository health:
|
|
||||||
- Repository structure validation
|
|
||||||
- Manifest validation
|
|
||||||
- Version alignment checks
|
|
||||||
- PHP syntax validation
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/run/smoke_test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```
|
|
||||||
INFO: Running smoke tests for Moko-Cassiopeia repository
|
|
||||||
INFO: Checking repository structure...
|
|
||||||
INFO: ✓ Repository structure valid
|
|
||||||
INFO: Checking Joomla manifest...
|
|
||||||
INFO: Found manifest: src/templates/templateDetails.xml
|
|
||||||
INFO: Extension: moko-cassiopeia (template) v03.05.00
|
|
||||||
INFO: ✓ Manifest validation passed
|
|
||||||
INFO: =========================================
|
|
||||||
INFO: Smoke tests completed successfully
|
|
||||||
INFO: =========================================
|
|
||||||
```
|
|
||||||
|
|
||||||
### `validate_all.sh`
|
|
||||||
Runs all validation scripts and provides a comprehensive report:
|
|
||||||
- Executes all required validation checks
|
|
||||||
- Executes all optional validation checks
|
|
||||||
- Provides colored output with pass/fail indicators
|
|
||||||
- Returns summary with counts
|
|
||||||
- Supports verbose mode for detailed output
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/run/validate_all.sh # Standard mode
|
|
||||||
./scripts/run/validate_all.sh -v # Verbose mode (shows all output)
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```
|
|
||||||
=== Repository Validation Suite ===
|
|
||||||
INFO: Running all validation checks...
|
|
||||||
|
|
||||||
=== Required Checks ===
|
|
||||||
[SUCCESS] ✓ manifest
|
|
||||||
[SUCCESS] ✓ xml_wellformed
|
|
||||||
|
|
||||||
=== Optional Checks ===
|
|
||||||
[SUCCESS] ✓ no_secrets
|
|
||||||
[SUCCESS] ✓ php_syntax
|
|
||||||
WARN: ✗ tabs (warnings/issues found - run with -v for details)
|
|
||||||
|
|
||||||
=== Validation Summary ===
|
|
||||||
Required checks passed: 2/2
|
|
||||||
Optional checks passed: 2/8
|
|
||||||
[SUCCESS] SUCCESS: All required checks passed
|
|
||||||
```
|
|
||||||
|
|
||||||
### `script_health.sh`
|
|
||||||
Validates that all scripts follow enterprise standards:
|
|
||||||
- Checks for copyright headers
|
|
||||||
- Validates SPDX license identifiers
|
|
||||||
- Ensures FILE INFORMATION sections are present
|
|
||||||
- Verifies error handling (set -euo pipefail)
|
|
||||||
- Checks executable permissions
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/run/script_health.sh # Standard mode
|
|
||||||
./scripts/run/script_health.sh -v # Verbose mode (shows details)
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```
|
|
||||||
=== Script Health Summary ===
|
|
||||||
Total scripts checked: 21
|
|
||||||
Missing copyright: 0
|
|
||||||
Missing SPDX identifier: 0
|
|
||||||
Missing FILE INFORMATION: 0
|
|
||||||
Missing error handling: 0
|
|
||||||
Not executable: 0
|
|
||||||
[SUCCESS] SUCCESS: All scripts follow enterprise standards
|
|
||||||
```
|
|
||||||
|
|
||||||
### `list_versions.sh`
|
|
||||||
Lists all version branches organized by prefix:
|
|
||||||
- Displays dev/, rc/, and version/ branches
|
|
||||||
- Shows versions sorted in ascending order
|
|
||||||
- Provides a summary count of each branch type
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/run/list_versions.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```
|
|
||||||
========================================
|
|
||||||
Version Branches Summary
|
|
||||||
========================================
|
|
||||||
|
|
||||||
📦 Stable Versions (version/)
|
|
||||||
----------------------------------------
|
|
||||||
✓ version/03.00.00
|
|
||||||
✓ version/03.01.00
|
|
||||||
|
|
||||||
🔧 Release Candidates (rc/)
|
|
||||||
----------------------------------------
|
|
||||||
➜ rc/03.02.00
|
|
||||||
|
|
||||||
🚧 Development Versions (dev/)
|
|
||||||
----------------------------------------
|
|
||||||
⚡ dev/03.05.00
|
|
||||||
⚡ dev/04.00.00
|
|
||||||
|
|
||||||
========================================
|
|
||||||
Total: 2 stable, 1 RC, 2 dev
|
|
||||||
========================================
|
|
||||||
```
|
|
||||||
|
|
||||||
### `check_version.sh`
|
|
||||||
Checks if a version can be created in a specific branch prefix:
|
|
||||||
- Validates against version hierarchy rules
|
|
||||||
- Checks for existing branches
|
|
||||||
- Reports conflicts with higher priority branches
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```bash
|
|
||||||
./scripts/run/check_version.sh <BRANCH_PREFIX> <VERSION>
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```bash
|
|
||||||
./scripts/run/check_version.sh dev/ 03.05.00
|
|
||||||
./scripts/run/check_version.sh rc/ 03.01.00
|
|
||||||
./scripts/run/check_version.sh version/ 02.00.00
|
|
||||||
```
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
- 0: Version can be created (no conflicts)
|
|
||||||
- 1: Version cannot be created (conflicts found)
|
|
||||||
- 2: Invalid arguments
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Enterprise Standards
|
|
||||||
|
|
||||||
For comprehensive enterprise-grade scripting standards, see
|
|
||||||
[ENTERPRISE.md](./ENTERPRISE.md).
|
|
||||||
|
|
||||||
Key highlights:
|
|
||||||
- **Error Handling**: Fail fast with clear, actionable messages
|
|
||||||
- **Security**: Input validation, no hardcoded secrets
|
|
||||||
- **Logging**: Structured output with timestamps
|
|
||||||
- **Portability**: Cross-platform compatibility
|
|
||||||
- **Documentation**: Usage functions and inline comments
|
|
||||||
|
|
||||||
### Writing New Scripts
|
|
||||||
|
|
||||||
1. **Use the library functions**:
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Include proper headers**:
|
|
||||||
- Copyright notice
|
|
||||||
- SPDX license identifier
|
|
||||||
- FILE INFORMATION section with DEFGROUP, INGROUP, PATH, VERSION, BRIEF
|
|
||||||
|
|
||||||
3. **Follow error handling conventions**:
|
|
||||||
```bash
|
|
||||||
[ -f "${FILE}" ] || die "File not found: ${FILE}"
|
|
||||||
require_cmd python3
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Use structured output**:
|
|
||||||
```bash
|
|
||||||
log_info "Starting process..."
|
|
||||||
log_success "Process completed"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Make scripts executable**:
|
|
||||||
```bash
|
|
||||||
chmod +x scripts/new-script.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Scripts Locally
|
|
||||||
|
|
||||||
Run all validation scripts:
|
|
||||||
```bash
|
|
||||||
./scripts/run/validate_all.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Run individual validation scripts:
|
|
||||||
```bash
|
|
||||||
./scripts/validate/manifest.sh
|
|
||||||
./scripts/validate/php_syntax.sh
|
|
||||||
./scripts/validate/tabs.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Run smoke tests:
|
|
||||||
```bash
|
|
||||||
./scripts/run/smoke_test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI Integration
|
|
||||||
|
|
||||||
Scripts are automatically executed in GitHub Actions workflows:
|
|
||||||
- `.github/workflows/ci.yml` - Continuous integration
|
|
||||||
- `.github/workflows/repo_health.yml` - Repository health checks
|
|
||||||
|
|
||||||
## Enterprise Features
|
|
||||||
|
|
||||||
The scripts in this repository follow enterprise-grade standards:
|
|
||||||
|
|
||||||
### Dependency Checking
|
|
||||||
|
|
||||||
Scripts validate required dependencies at startup using `check_dependencies`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
check_dependencies python3 git sed
|
# Run all validations
|
||||||
|
make validate
|
||||||
|
python3 scripts/run/validate_all.py
|
||||||
|
|
||||||
|
# Run specific validations
|
||||||
|
python3 scripts/validate/manifest.py
|
||||||
|
python3 scripts/validate/xml_wellformed.py
|
||||||
|
|
||||||
|
# Create distribution package (auto-detects Joomla or Dolibarr)
|
||||||
|
make package
|
||||||
|
python3 scripts/release/package_extension.py dist
|
||||||
```
|
```
|
||||||
|
|
||||||
### Timestamp Logging
|
## Platform Support
|
||||||
|
|
||||||
All major operations include timestamps for audit trails:
|
All scripts automatically detect and support:
|
||||||
|
- **Joomla Extensions**: Templates, Components, Modules, Plugins, Packages
|
||||||
|
- **Dolibarr Modules**: Automatic detection and packaging
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
### Validation Scripts (`scripts/validate/`)
|
||||||
|
- `manifest.py` - Validate extension manifests (Joomla/Dolibarr)
|
||||||
|
- `xml_wellformed.py` - Validate XML syntax
|
||||||
|
- `workflows.py` - Validate GitHub Actions workflows
|
||||||
|
- `tabs.py` - Check for tab characters in YAML
|
||||||
|
- `no_secrets.py` - Scan for secrets/credentials
|
||||||
|
- `paths.py` - Check for Windows-style paths
|
||||||
|
- `php_syntax.py` - Validate PHP syntax
|
||||||
|
|
||||||
|
### Release Scripts (`scripts/release/`)
|
||||||
|
- `package_extension.py` - Create distributable ZIP packages
|
||||||
|
|
||||||
|
### Run Scripts (`scripts/run/`)
|
||||||
|
- `validate_all.py` - Run all validation scripts
|
||||||
|
- `scaffold_extension.py` - Create new extension scaffolding
|
||||||
|
|
||||||
|
### Library Scripts (`scripts/lib/`)
|
||||||
|
- `common.py` - Common utilities
|
||||||
|
- `joomla_manifest.py` - Joomla manifest parsing
|
||||||
|
- `dolibarr_manifest.py` - Dolibarr module parsing
|
||||||
|
- `extension_utils.py` - Unified extension detection
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.6+
|
||||||
|
- Git
|
||||||
|
- PHP (for PHP syntax validation)
|
||||||
|
|
||||||
|
## Migration from Shell Scripts
|
||||||
|
|
||||||
|
All shell scripts have been converted to Python. Use Python equivalents:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
log_info "Start time: $(log_timestamp)"
|
# Old (removed) # New
|
||||||
|
./scripts/validate/manifest.sh → python3 scripts/validate/manifest.py
|
||||||
|
./scripts/release/package.sh → python3 scripts/release/package_extension.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage Documentation
|
For detailed documentation, see individual script help:
|
||||||
|
|
||||||
All user-facing scripts include comprehensive help:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/run/validate_all.sh --help
|
python3 scripts/validate/manifest.py --help
|
||||||
./scripts/fix/versions.sh --help
|
python3 scripts/release/package_extension.py --help
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standardized Exit Codes
|
## License
|
||||||
|
|
||||||
- `0` - Success
|
GPL-3.0-or-later - See [LICENSE](../LICENSE)
|
||||||
- `1` - Fatal error
|
|
||||||
- `2` - Invalid arguments
|
|
||||||
|
|
||||||
### Enhanced Error Messages
|
|
||||||
|
|
||||||
Clear, actionable error messages with context:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
die "Required file not found: ${CONFIG_FILE}. Run setup first."
|
|
||||||
```
|
|
||||||
|
|
||||||
See [ENTERPRISE.md](./ENTERPRISE.md) for complete standards documentation.
|
|
||||||
|
|
||||||
## Version History
|
|
||||||
|
|
||||||
| Version | Date | Description |
|
|
||||||
| ------- | ---------- | ------------------------------------- |
|
|
||||||
| 01.00.00 | 2025-01-03 | Initial scripts documentation created |
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
- **Document:** scripts/README.md
|
|
||||||
- **Repository:** https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
- **Version:** 01.00.00
|
|
||||||
- **Status:** Active
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# 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 (./LICENSE).
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoStandards
|
|
||||||
# INGROUP: Generic.Script
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /scripts/fix/paths.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Replace Windows-style path separators with POSIX separators in text files.#
|
|
||||||
# Purpose:
|
|
||||||
# - Normalize path separators in text files to forward slashes (/).
|
|
||||||
# - Intended for CI validation and optional remediation workflows.
|
|
||||||
# - Skips binary files and version control metadata.
|
|
||||||
# - Preserves file contents aside from path separator normalization.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/fix/paths.sh
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="${1:-.}"
|
|
||||||
|
|
||||||
info() {
|
|
||||||
echo "INFO: $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
warn() {
|
|
||||||
echo "WARN: $*" 1>&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "ERROR: $*" 1>&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
command -v find >/dev/null 2>&1 || die "find not available"
|
|
||||||
command -v sed >/dev/null 2>&1 || die "sed not available"
|
|
||||||
command -v file >/dev/null 2>&1 || die "file not available"
|
|
||||||
|
|
||||||
info "Scanning for text files under: $ROOT_DIR"
|
|
||||||
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if file "$file" | grep -qi "text"; then
|
|
||||||
if grep -q '\\\\' "$file"; then
|
|
||||||
sed -i.bak 's#\\\\#/#g' "$file" && rm -f "$file.bak"
|
|
||||||
info "Normalized paths in $file"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done < <(
|
|
||||||
find "$ROOT_DIR" \
|
|
||||||
-type f \
|
|
||||||
-not -path "*/.git/*" \
|
|
||||||
-not -path "*/node_modules/*" \
|
|
||||||
-print0
|
|
||||||
)
|
|
||||||
|
|
||||||
info "Path normalization complete."
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# ============================================================================
|
|
||||||
# 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. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoStandards
|
|
||||||
# INGROUP: GitHub.Actions.Utilities
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /scripts/fix/tabs.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Utility script to replace tab characters with two spaces in YAML files.
|
|
||||||
# NOTE: Intended for local developer use. Not executed automatically in CI.
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '%s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
log "[fix/tabs] Scope: *.yml, *.yaml"
|
|
||||||
log "[fix/tabs] Action: replace tab characters with two spaces"
|
|
||||||
|
|
||||||
changed=0
|
|
||||||
|
|
||||||
# Determine file list
|
|
||||||
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
files=$(git ls-files '*.yml' '*.yaml' 2>/dev/null || true)
|
|
||||||
else
|
|
||||||
files=$(find . -type f \( -name '*.yml' -o -name '*.yaml' \) -print 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${files}" ]; then
|
|
||||||
log "[fix/tabs] No YAML files found. Nothing to fix."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r f; do
|
|
||||||
[ -n "$f" ] || continue
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
|
|
||||||
if LC_ALL=C grep -q $'\t' -- "$f"; then
|
|
||||||
log "[fix/tabs] Fixing tabs in: $f"
|
|
||||||
# Replace literal tab characters with exactly two spaces
|
|
||||||
sed -i 's/\t/ /g' "$f"
|
|
||||||
changed=1
|
|
||||||
else
|
|
||||||
log "[fix/tabs] Clean: $f"
|
|
||||||
fi
|
|
||||||
done <<< "${files}"
|
|
||||||
|
|
||||||
if [ "$changed" -eq 1 ]; then
|
|
||||||
log "[fix/tabs] Completed with modifications"
|
|
||||||
else
|
|
||||||
log "[fix/tabs] No changes required"
|
|
||||||
fi
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.Fix
|
|
||||||
# INGROUP: Version.Management
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/fix/versions.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Update version numbers across repository files
|
|
||||||
# NOTE: Updates manifest, package.json, and other version references
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Usage and validation
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<-USAGE
|
|
||||||
Usage: $0 <VERSION>
|
|
||||||
|
|
||||||
Update version numbers across repository files.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
VERSION Semantic version in format X.Y.Z (e.g., 3.5.0)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$0 3.5.0
|
|
||||||
$0 1.2.3
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
0 - Version updated successfully
|
|
||||||
1 - Invalid version format or update failed
|
|
||||||
2 - Invalid arguments
|
|
||||||
|
|
||||||
Files updated:
|
|
||||||
- Joomla manifest XML (<version> tag)
|
|
||||||
- package.json (if present)
|
|
||||||
- README.md (VERSION: references, if present)
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_version() {
|
|
||||||
local v="$1"
|
|
||||||
if ! printf '%s' "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
die "Invalid version format: $v (expected X.Y.Z)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ $# -eq 1 ] || usage
|
|
||||||
|
|
||||||
VERSION="$1"
|
|
||||||
validate_version "${VERSION}"
|
|
||||||
|
|
||||||
# Check dependencies
|
|
||||||
check_dependencies python3
|
|
||||||
|
|
||||||
log_info "Updating version to: ${VERSION}"
|
|
||||||
log_info "Start time: $(log_timestamp)"
|
|
||||||
|
|
||||||
# Source Joomla manifest utilities
|
|
||||||
. "${SCRIPT_DIR}/lib/joomla_manifest.sh"
|
|
||||||
|
|
||||||
# Find and update manifest
|
|
||||||
MANIFEST="$(find_manifest src)"
|
|
||||||
log_info "Updating manifest: ${MANIFEST}"
|
|
||||||
|
|
||||||
# Cross-platform sed helper
|
|
||||||
sed_inplace() {
|
|
||||||
local expr="$1"
|
|
||||||
local file="$2"
|
|
||||||
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i -E "${expr}" "${file}"
|
|
||||||
else
|
|
||||||
sed -i '' -E "${expr}" "${file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update version in manifest XML
|
|
||||||
if grep -q '<version>' "${MANIFEST}"; then
|
|
||||||
sed_inplace "s|<version>[^<]*</version>|<version>${VERSION}</version>|g" "${MANIFEST}"
|
|
||||||
log_info "✓ Updated manifest version"
|
|
||||||
else
|
|
||||||
log_warn "No <version> tag found in manifest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update package.json if it exists
|
|
||||||
if [ -f "package.json" ]; then
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
python3 - <<PY "${VERSION}"
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
version = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open('package.json', 'r') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
data['version'] = version
|
|
||||||
|
|
||||||
with open('package.json', 'w') as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
f.write('\n')
|
|
||||||
|
|
||||||
print(f"✓ Updated package.json to version {version}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"WARN: Failed to update package.json: {e}")
|
|
||||||
sys.exit(0)
|
|
||||||
PY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update README.md version references
|
|
||||||
if [ -f "README.md" ]; then
|
|
||||||
# Look for version references in format VERSION: XX.XX.XX
|
|
||||||
if grep -q 'VERSION: [0-9]' README.md; then
|
|
||||||
# Validate version format has 3 components
|
|
||||||
component_count=$(echo "${VERSION}" | awk -F. '{print NF}')
|
|
||||||
if [ "${component_count}" -eq 3 ]; then
|
|
||||||
# Convert to zero-padded format
|
|
||||||
PADDED_VERSION="$(printf '%s' "${VERSION}" | awk -F. '{printf "%02d.%02d.%02d", $1, $2, $3}')"
|
|
||||||
sed_inplace "s|VERSION: [0-9]{2}\.[0-9]{2}\.[0-9]{2}|VERSION: ${PADDED_VERSION}|g" README.md
|
|
||||||
log_info "✓ Updated README.md version references"
|
|
||||||
else
|
|
||||||
log_warn "Version format invalid for padding, skipping README.md update"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "========================================="
|
|
||||||
log_info "Version update completed: ${VERSION}"
|
|
||||||
log_info "End time: $(log_timestamp)"
|
|
||||||
log_info "Files updated:"
|
|
||||||
log_info " - ${MANIFEST}"
|
|
||||||
[ -f "package.json" ] && log_info " - package.json"
|
|
||||||
[ -f "README.md" ] && log_info " - README.md"
|
|
||||||
log_info "========================================="
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Install Git hooks for Moko Cassiopeia
|
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Moko-Cassiopeia.Scripts
|
|
||||||
# INGROUP: Scripts.Git
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# FILE: ./scripts/git/install-hooks.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Install Git hooks for local development
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
echo "Installing Git hooks..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Create .git/hooks directory if it doesn't exist
|
|
||||||
mkdir -p "${REPO_ROOT}/.git/hooks"
|
|
||||||
|
|
||||||
# Install pre-commit hook
|
|
||||||
PRE_COMMIT_HOOK="${REPO_ROOT}/.git/hooks/pre-commit"
|
|
||||||
cat > "${PRE_COMMIT_HOOK}" <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Pre-commit hook - installed by scripts/git/install-hooks.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(git rev-parse --show-toplevel)/scripts/git"
|
|
||||||
|
|
||||||
if [ -f "${SCRIPT_DIR}/pre-commit.sh" ]; then
|
|
||||||
exec "${SCRIPT_DIR}/pre-commit.sh" "$@"
|
|
||||||
else
|
|
||||||
echo "Error: pre-commit.sh not found in ${SCRIPT_DIR}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x "${PRE_COMMIT_HOOK}"
|
|
||||||
|
|
||||||
echo "✓ Installed pre-commit hook"
|
|
||||||
echo ""
|
|
||||||
echo "The pre-commit hook will run automatically before each commit."
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " - Skip hook: git commit --no-verify"
|
|
||||||
echo " - Quick mode: ./scripts/git/pre-commit.sh --quick"
|
|
||||||
echo " - Skip quality checks: ./scripts/git/pre-commit.sh --skip-quality"
|
|
||||||
echo ""
|
|
||||||
echo "To uninstall hooks:"
|
|
||||||
echo " rm .git/hooks/pre-commit"
|
|
||||||
echo ""
|
|
||||||
echo "Done!"
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Pre-commit hook script for Moko Cassiopeia
|
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Moko-Cassiopeia.Scripts
|
|
||||||
# INGROUP: Scripts.Git
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# FILE: ./scripts/git/pre-commit.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Pre-commit hook for local validation
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}✓${NC} $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
|
||||||
echo -e "${YELLOW}⚠${NC} $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}✗${NC} $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_header() {
|
|
||||||
echo ""
|
|
||||||
echo "================================"
|
|
||||||
echo "$*"
|
|
||||||
echo "================================"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
SKIP_TESTS=false
|
|
||||||
SKIP_QUALITY=false
|
|
||||||
QUICK_MODE=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--skip-tests)
|
|
||||||
SKIP_TESTS=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--skip-quality)
|
|
||||||
SKIP_QUALITY=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--quick)
|
|
||||||
QUICK_MODE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
echo "Usage: pre-commit.sh [--skip-tests] [--skip-quality] [--quick]"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
log_header "Pre-commit Validation"
|
|
||||||
|
|
||||||
# Get list of staged files
|
|
||||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
|
|
||||||
|
|
||||||
if [ -z "$STAGED_FILES" ]; then
|
|
||||||
log_warning "No staged files to check"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Checking staged files:"
|
|
||||||
echo "$STAGED_FILES" | sed 's/^/ - /'
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Track failures
|
|
||||||
FAILURES=0
|
|
||||||
|
|
||||||
# Check 1: PHP Syntax
|
|
||||||
log_header "Checking PHP Syntax"
|
|
||||||
PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true)
|
|
||||||
|
|
||||||
if [ -n "$PHP_FILES" ]; then
|
|
||||||
while IFS= read -r file; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
if php -l "$file" > /dev/null 2>&1; then
|
|
||||||
log_success "PHP syntax OK: $file"
|
|
||||||
else
|
|
||||||
log_error "PHP syntax error: $file"
|
|
||||||
php -l "$file"
|
|
||||||
FAILURES=$((FAILURES + 1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$PHP_FILES"
|
|
||||||
else
|
|
||||||
echo " No PHP files to check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 2: XML Well-formedness
|
|
||||||
log_header "Checking XML Files"
|
|
||||||
XML_FILES=$(echo "$STAGED_FILES" | grep '\.xml$' || true)
|
|
||||||
|
|
||||||
if [ -n "$XML_FILES" ]; then
|
|
||||||
while IFS= read -r file; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
if xmllint --noout "$file" 2>/dev/null; then
|
|
||||||
log_success "XML well-formed: $file"
|
|
||||||
else
|
|
||||||
log_error "XML malformed: $file"
|
|
||||||
xmllint --noout "$file" || true
|
|
||||||
FAILURES=$((FAILURES + 1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$XML_FILES"
|
|
||||||
else
|
|
||||||
echo " No XML files to check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 3: YAML Syntax
|
|
||||||
log_header "Checking YAML Files"
|
|
||||||
YAML_FILES=$(echo "$STAGED_FILES" | grep -E '\.(yml|yaml)$' || true)
|
|
||||||
|
|
||||||
if [ -n "$YAML_FILES" ]; then
|
|
||||||
while IFS= read -r file; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
# Use printf to safely pass the file path, avoiding injection
|
|
||||||
if python3 -c "import sys, yaml; yaml.safe_load(open(sys.argv[1]))" "$file" 2>/dev/null; then
|
|
||||||
log_success "YAML valid: $file"
|
|
||||||
else
|
|
||||||
log_error "YAML invalid: $file"
|
|
||||||
python3 -c "import sys, yaml; yaml.safe_load(open(sys.argv[1]))" "$file" || true
|
|
||||||
FAILURES=$((FAILURES + 1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$YAML_FILES"
|
|
||||||
else
|
|
||||||
echo " No YAML files to check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 4: Trailing Whitespace
|
|
||||||
log_header "Checking for Trailing Whitespace"
|
|
||||||
TEXT_FILES=$(echo "$STAGED_FILES" | grep -vE '\.(png|jpg|jpeg|gif|svg|ico|zip|gz|woff|woff2|ttf)$' || true)
|
|
||||||
|
|
||||||
if [ -n "$TEXT_FILES" ]; then
|
|
||||||
TRAILING_WS=$(echo "$TEXT_FILES" | xargs grep -n '[[:space:]]$' 2>/dev/null || true)
|
|
||||||
if [ -n "$TRAILING_WS" ]; then
|
|
||||||
log_warning "Files with trailing whitespace found:"
|
|
||||||
echo "$TRAILING_WS" | sed 's/^/ /'
|
|
||||||
echo ""
|
|
||||||
echo " Run: sed -i 's/[[:space:]]*$//' <file> to fix"
|
|
||||||
else
|
|
||||||
log_success "No trailing whitespace"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " No text files to check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 5: SPDX License Headers (if not quick mode)
|
|
||||||
if [ "$QUICK_MODE" = false ]; then
|
|
||||||
log_header "Checking SPDX License Headers"
|
|
||||||
SOURCE_FILES=$(echo "$STAGED_FILES" | grep -E '\.(php|sh|js|ts|css)$' || true)
|
|
||||||
|
|
||||||
if [ -n "$SOURCE_FILES" ]; then
|
|
||||||
MISSING_SPDX=""
|
|
||||||
while IFS= read -r file; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
if ! head -n 20 "$file" | grep -q 'SPDX-License-Identifier:'; then
|
|
||||||
MISSING_SPDX="${MISSING_SPDX} - ${file}\n"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$SOURCE_FILES"
|
|
||||||
|
|
||||||
if [ -n "$MISSING_SPDX" ]; then
|
|
||||||
log_warning "Files missing SPDX license header:"
|
|
||||||
echo -e "$MISSING_SPDX"
|
|
||||||
else
|
|
||||||
log_success "All source files have SPDX headers"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " No source files to check"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 6: No Secrets
|
|
||||||
log_header "Checking for Secrets"
|
|
||||||
if [ -x "${SCRIPT_DIR}/validate/no_secrets.sh" ]; then
|
|
||||||
if "${SCRIPT_DIR}/validate/no_secrets.sh" > /dev/null 2>&1; then
|
|
||||||
log_success "No secrets detected"
|
|
||||||
else
|
|
||||||
log_error "Potential secrets detected!"
|
|
||||||
"${SCRIPT_DIR}/validate/no_secrets.sh" || true
|
|
||||||
FAILURES=$((FAILURES + 1))
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " Secret scanner not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 7: PHP_CodeSniffer (if not skipped)
|
|
||||||
if [ "$SKIP_QUALITY" = false ] && command -v phpcs >/dev/null 2>&1; then
|
|
||||||
log_header "Running PHP_CodeSniffer"
|
|
||||||
PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true)
|
|
||||||
|
|
||||||
if [ -n "$PHP_FILES" ]; then
|
|
||||||
# Use process substitution to avoid issues with filenames containing spaces
|
|
||||||
if echo "$PHP_FILES" | tr '\n' '\0' | xargs -0 phpcs --standard=phpcs.xml -q 2>/dev/null; then
|
|
||||||
log_success "PHPCS passed"
|
|
||||||
else
|
|
||||||
log_warning "PHPCS found issues (non-blocking)"
|
|
||||||
echo "$PHP_FILES" | tr '\n' '\0' | xargs -0 phpcs --standard=phpcs.xml --report=summary || true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " No PHP files to check"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ "$SKIP_QUALITY" = true ]; then
|
|
||||||
echo " Skipping PHPCS (--skip-quality)"
|
|
||||||
else
|
|
||||||
echo " PHPCS not available (install with: composer global require squizlabs/php_codesniffer)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check 8: PHPStan (if not skipped and not quick mode)
|
|
||||||
if [ "$SKIP_QUALITY" = false ] && [ "$QUICK_MODE" = false ] && command -v phpstan >/dev/null 2>&1; then
|
|
||||||
log_header "Running PHPStan"
|
|
||||||
PHP_FILES=$(echo "$STAGED_FILES" | grep '\.php$' || true)
|
|
||||||
|
|
||||||
if [ -n "$PHP_FILES" ]; then
|
|
||||||
if phpstan analyse --configuration=phpstan.neon --no-progress > /dev/null 2>&1; then
|
|
||||||
log_success "PHPStan passed"
|
|
||||||
else
|
|
||||||
log_warning "PHPStan found issues (non-blocking)"
|
|
||||||
phpstan analyse --configuration=phpstan.neon --no-progress || true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " No PHP files to check"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ "$SKIP_QUALITY" = true ]; then
|
|
||||||
echo " Skipping PHPStan (--skip-quality)"
|
|
||||||
elif [ "$QUICK_MODE" = true ]; then
|
|
||||||
echo " Skipping PHPStan (--quick mode)"
|
|
||||||
else
|
|
||||||
echo " PHPStan not available (install with: composer global require phpstan/phpstan)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
log_header "Pre-commit Summary"
|
|
||||||
|
|
||||||
if [ $FAILURES -gt 0 ]; then
|
|
||||||
log_error "Pre-commit checks failed with $FAILURES error(s)"
|
|
||||||
echo ""
|
|
||||||
echo "To commit anyway, use: git commit --no-verify"
|
|
||||||
echo "To run quick checks only: ./scripts/git/pre-commit.sh --quick"
|
|
||||||
echo "To skip quality checks: ./scripts/git/pre-commit.sh --skip-quality"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
log_success "All pre-commit checks passed!"
|
|
||||||
echo ""
|
|
||||||
echo "Tip: Use 'make validate' for comprehensive validation"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program (./LICENSE.md).
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FILE INFORMATION
|
|
||||||
# ============================================================================
|
|
||||||
# DEFGROUP: Script.Library
|
|
||||||
# INGROUP: Common
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech
|
|
||||||
# PATH: /scripts/lib/common.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Unified shared shell utilities for all CI and local scripts
|
|
||||||
# NOTE:
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Environment normalization
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export LC_ALL=C
|
|
||||||
export LANG=C
|
|
||||||
|
|
||||||
is_ci() {
|
|
||||||
[ "${CI:-}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
|
||||||
printf '%s\n' "ERROR: Required command not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Logging
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
printf '%s\n' "INFO: $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warn() {
|
|
||||||
printf '%s\n' "WARN: $*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
printf '%s\n' "ERROR: $*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
log_error "$*"
|
|
||||||
if [ "${VERBOSE_ERRORS:-true}" = "true" ]; then
|
|
||||||
echo "" >&2
|
|
||||||
echo "Stack trace (last 10 commands):" >&2
|
|
||||||
if [ -n "${BASH_VERSION:-}" ]; then
|
|
||||||
history | tail -10 >&2 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
echo "" >&2
|
|
||||||
echo "Environment:" >&2
|
|
||||||
echo " PWD: $(pwd)" >&2
|
|
||||||
echo " USER: ${USER:-unknown}" >&2
|
|
||||||
echo " SHELL: ${SHELL:-unknown}" >&2
|
|
||||||
echo " CI: ${CI:-false}" >&2
|
|
||||||
echo "" >&2
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Validation helpers
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
assert_file_exists() {
|
|
||||||
[ -f "$1" ] || die "Required file missing: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_dir_exists() {
|
|
||||||
[ -d "$1" ] || die "Required directory missing: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_non_empty() {
|
|
||||||
[ -n "${1:-}" ] || die "Expected non empty value"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Path helpers
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
script_root() {
|
|
||||||
cd "$(dirname "$0")/.." && pwd
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_path() {
|
|
||||||
printf '%s\n' "$1" | sed 's|\\|/|g'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# JSON utilities
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
require_cmd python3
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
json_output() {
|
|
||||||
local status="$1"
|
|
||||||
shift
|
|
||||||
require_cmd python3
|
|
||||||
python3 - <<PY "$status" "$@"
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
status = sys.argv[1]
|
|
||||||
pairs = sys.argv[2:]
|
|
||||||
data = {"status": status}
|
|
||||||
for pair in pairs:
|
|
||||||
if "=" in pair:
|
|
||||||
k, v = pair.split("=", 1)
|
|
||||||
data[k] = v
|
|
||||||
print(json.dumps(data, ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Guardrails
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fail_if_root() {
|
|
||||||
[ "$(id -u)" -eq 0 ] && die "Script must not run as root"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Enterprise features
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Check for required dependencies at script start
|
|
||||||
check_dependencies() {
|
|
||||||
local missing=0
|
|
||||||
local missing_cmds=()
|
|
||||||
|
|
||||||
for cmd in "$@"; do
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
log_error "Required command not found: $cmd"
|
|
||||||
missing=$((missing + 1))
|
|
||||||
missing_cmds+=("$cmd")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$missing" -gt 0 ]; then
|
|
||||||
echo "" >&2
|
|
||||||
echo "Missing required dependencies:" >&2
|
|
||||||
for cmd in "${missing_cmds[@]}"; do
|
|
||||||
echo " - $cmd" >&2
|
|
||||||
done
|
|
||||||
echo "" >&2
|
|
||||||
echo "Installation guides:" >&2
|
|
||||||
for cmd in "${missing_cmds[@]}"; do
|
|
||||||
case "$cmd" in
|
|
||||||
python3)
|
|
||||||
echo " python3: apt-get install python3 (Debian/Ubuntu) or brew install python3 (macOS)" >&2
|
|
||||||
;;
|
|
||||||
git)
|
|
||||||
echo " git: apt-get install git (Debian/Ubuntu) or brew install git (macOS)" >&2
|
|
||||||
;;
|
|
||||||
php)
|
|
||||||
echo " php: apt-get install php-cli (Debian/Ubuntu) or brew install php (macOS)" >&2
|
|
||||||
;;
|
|
||||||
xmllint)
|
|
||||||
echo " xmllint: apt-get install libxml2-utils (Debian/Ubuntu) or brew install libxml2 (macOS)" >&2
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo " $cmd: Please install via your system package manager" >&2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
echo "" >&2
|
|
||||||
die "Missing $missing required command(s)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Timeout wrapper for long-running commands
|
|
||||||
run_with_timeout() {
|
|
||||||
local timeout="$1"
|
|
||||||
shift
|
|
||||||
if command -v timeout >/dev/null 2>&1; then
|
|
||||||
timeout "$timeout" "$@"
|
|
||||||
else
|
|
||||||
"$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add script execution timestamp
|
|
||||||
log_timestamp() {
|
|
||||||
if command -v date >/dev/null 2>&1; then
|
|
||||||
printf '%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Calculate and log execution duration
|
|
||||||
log_duration() {
|
|
||||||
local start="$1"
|
|
||||||
local end="$2"
|
|
||||||
local duration=$((end - start))
|
|
||||||
if [ "$duration" -ge 60 ]; then
|
|
||||||
local minutes=$((duration / 60))
|
|
||||||
local seconds=$((duration % 60))
|
|
||||||
printf '%dm %ds\n' "$minutes" "$seconds"
|
|
||||||
else
|
|
||||||
printf '%ds\n' "$duration"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program (./LICENSE.md).
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FILE INFORMATION
|
|
||||||
# ============================================================================
|
|
||||||
# DEFGROUP: Script.Library
|
|
||||||
# INGROUP: Joomla.Manifest
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech
|
|
||||||
# PATH: /scripts/lib/joomla_manifest.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Joomla manifest parsing and validation utilities
|
|
||||||
# NOTE: Provides reusable functions for working with Joomla extension manifests
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# Resolve script directory properly - works when sourced
|
|
||||||
if [ -n "${SCRIPT_DIR:-}" ]; then
|
|
||||||
# Already set by caller
|
|
||||||
SCRIPT_LIB_DIR="${SCRIPT_DIR}/lib"
|
|
||||||
else
|
|
||||||
# Determine from this file's location
|
|
||||||
SCRIPT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Shared utilities
|
|
||||||
. "${SCRIPT_LIB_DIR}/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Manifest discovery
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Find the primary Joomla manifest in the given directory
|
|
||||||
# Usage: find_manifest <src_dir>
|
|
||||||
# Returns: path to manifest file or exits with error
|
|
||||||
find_manifest() {
|
|
||||||
local src_dir="${1:-src}"
|
|
||||||
|
|
||||||
[ -d "${src_dir}" ] || die "Source directory missing: ${src_dir}"
|
|
||||||
|
|
||||||
# Candidate discovery policy: prefer explicit known names
|
|
||||||
local candidates=""
|
|
||||||
|
|
||||||
# Template
|
|
||||||
if [ -f "${src_dir}/templateDetails.xml" ]; then
|
|
||||||
candidates="${src_dir}/templateDetails.xml"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Package
|
|
||||||
if [ -z "${candidates}" ]; then
|
|
||||||
candidates="$(find "${src_dir}" -maxdepth 4 -type f -name 'pkg_*.xml' 2>/dev/null | head -1 || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Component
|
|
||||||
if [ -z "${candidates}" ]; then
|
|
||||||
candidates="$(find "${src_dir}" -maxdepth 4 -type f -name 'com_*.xml' 2>/dev/null | head -1 || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Module
|
|
||||||
if [ -z "${candidates}" ]; then
|
|
||||||
candidates="$(find "${src_dir}" -maxdepth 4 -type f -name 'mod_*.xml' 2>/dev/null | head -1 || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Plugin
|
|
||||||
if [ -z "${candidates}" ]; then
|
|
||||||
candidates="$(find "${src_dir}" -maxdepth 6 -type f -name 'plg_*.xml' 2>/dev/null | head -1 || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fallback: any XML containing <extension ...>
|
|
||||||
if [ -z "${candidates}" ]; then
|
|
||||||
candidates="$(grep -Rsl --include='*.xml' '<extension' "${src_dir}" 2>/dev/null | head -1 || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${candidates}" ] || die "No Joomla manifest XML found under ${src_dir}"
|
|
||||||
[ -s "${candidates}" ] || die "Manifest is empty: ${candidates}"
|
|
||||||
|
|
||||||
printf '%s\n' "${candidates}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Manifest parsing
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Extract version from manifest XML
|
|
||||||
# Usage: get_manifest_version <manifest_path>
|
|
||||||
# Returns: version string or exits with error
|
|
||||||
get_manifest_version() {
|
|
||||||
local manifest="$1"
|
|
||||||
|
|
||||||
[ -f "${manifest}" ] || die "Manifest not found: ${manifest}"
|
|
||||||
|
|
||||||
require_cmd python3
|
|
||||||
|
|
||||||
python3 - "${manifest}" <<'PY'
|
|
||||||
import sys
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
manifest_path = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ET.parse(manifest_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
version_el = root.find("version")
|
|
||||||
if version_el is not None and version_el.text:
|
|
||||||
print(version_el.text.strip())
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(1)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract extension name from manifest XML
|
|
||||||
# Usage: get_manifest_name <manifest_path>
|
|
||||||
# Returns: name string or exits with error
|
|
||||||
get_manifest_name() {
|
|
||||||
local manifest="$1"
|
|
||||||
|
|
||||||
[ -f "${manifest}" ] || die "Manifest not found: ${manifest}"
|
|
||||||
|
|
||||||
require_cmd python3
|
|
||||||
|
|
||||||
python3 - "${manifest}" <<'PY'
|
|
||||||
import sys
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
manifest_path = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ET.parse(manifest_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
name_el = root.find("name")
|
|
||||||
if name_el is not None and name_el.text:
|
|
||||||
print(name_el.text.strip())
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(1)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract extension type from manifest XML
|
|
||||||
# Usage: get_manifest_type <manifest_path>
|
|
||||||
# Returns: type string (template, component, module, plugin, etc.) or exits with error
|
|
||||||
get_manifest_type() {
|
|
||||||
local manifest="$1"
|
|
||||||
|
|
||||||
[ -f "${manifest}" ] || die "Manifest not found: ${manifest}"
|
|
||||||
|
|
||||||
require_cmd python3
|
|
||||||
|
|
||||||
python3 - "${manifest}" <<'PY'
|
|
||||||
import sys
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
manifest_path = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ET.parse(manifest_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
ext_type = root.attrib.get("type", "").strip().lower()
|
|
||||||
if ext_type:
|
|
||||||
print(ext_type)
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(1)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# This file is part of a Moko Consulting project.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program (./LICENSE.md).
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FILE INFORMATION
|
|
||||||
# ============================================================================
|
|
||||||
# DEFGROUP: Script.Library
|
|
||||||
# INGROUP: Logging
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech
|
|
||||||
# PATH: /scripts/lib/logging.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Enhanced logging utilities with structured output support
|
|
||||||
# NOTE: Provides colored output, log levels, and structured logging
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# Resolve script directory properly - works when sourced
|
|
||||||
if [ -n "${SCRIPT_DIR:-}" ]; then
|
|
||||||
# Already set by caller
|
|
||||||
SCRIPT_LIB_DIR="${SCRIPT_DIR}/lib"
|
|
||||||
else
|
|
||||||
# Determine from this file's location
|
|
||||||
SCRIPT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Shared utilities
|
|
||||||
. "${SCRIPT_LIB_DIR}/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Color codes (if terminal supports it)
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Check if we're in a terminal and colors are supported
|
|
||||||
use_colors() {
|
|
||||||
[ -t 1 ] && [ "${CI:-false}" != "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
if use_colors; then
|
|
||||||
COLOR_RESET='\033[0m'
|
|
||||||
COLOR_RED='\033[0;31m'
|
|
||||||
COLOR_YELLOW='\033[0;33m'
|
|
||||||
COLOR_GREEN='\033[0;32m'
|
|
||||||
COLOR_BLUE='\033[0;34m'
|
|
||||||
COLOR_CYAN='\033[0;36m'
|
|
||||||
else
|
|
||||||
COLOR_RESET=''
|
|
||||||
COLOR_RED=''
|
|
||||||
COLOR_YELLOW=''
|
|
||||||
COLOR_GREEN=''
|
|
||||||
COLOR_BLUE=''
|
|
||||||
COLOR_CYAN=''
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Enhanced logging functions
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_debug() {
|
|
||||||
if [ "${DEBUG:-false}" = "true" ]; then
|
|
||||||
printf '%b[DEBUG]%b %s\n' "${COLOR_CYAN}" "${COLOR_RESET}" "$*"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
printf '%b[SUCCESS]%b %s\n' "${COLOR_GREEN}" "${COLOR_RESET}" "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_step() {
|
|
||||||
printf '%b[STEP]%b %s\n' "${COLOR_BLUE}" "${COLOR_RESET}" "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Structured logging
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Log a key-value pair
|
|
||||||
log_kv() {
|
|
||||||
local key="$1"
|
|
||||||
local value="$2"
|
|
||||||
printf ' %b%s:%b %s\n' "${COLOR_BLUE}" "${key}" "${COLOR_RESET}" "${value}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Log a list item
|
|
||||||
log_item() {
|
|
||||||
printf ' %b•%b %s\n' "${COLOR_GREEN}" "${COLOR_RESET}" "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Log a separator line
|
|
||||||
log_separator() {
|
|
||||||
printf '%s\n' "========================================="
|
|
||||||
}
|
|
||||||
|
|
||||||
# Log a section header
|
|
||||||
log_section() {
|
|
||||||
printf '\n%b=== %s ===%b\n' "${COLOR_BLUE}" "$*" "${COLOR_RESET}"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import common
|
import common
|
||||||
import joomla_manifest
|
import extension_utils
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
This import will fail because the This import will fail because the `extension_utils` module is not included in the pull request. The PR description mentions adding this file, but it's missing from the diffs. This will cause the script to exit immediately with an ImportError.
```suggestion
```
|
|||||||
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
print("ERROR: Cannot import required libraries", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -121,7 +121,7 @@ def create_package(
|
|||||||
exclude_patterns: Set[str] = None
|
exclude_patterns: Set[str] = None
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""
|
"""
|
||||||
Create a distributable ZIP package for a Joomla extension.
|
Create a distributable ZIP package for a Joomla or Dolibarr extension.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
src_dir: Source directory containing extension files
|
src_dir: Source directory containing extension files
|
||||||
@@ -137,13 +137,15 @@ def create_package(
|
|||||||
if not src_path.is_dir():
|
if not src_path.is_dir():
|
||||||
common.die(f"Source directory not found: {src_dir}")
|
common.die(f"Source directory not found: {src_dir}")
|
||||||
|
|
||||||
# Find and parse manifest
|
# Detect extension platform and get info
|
||||||
manifest_path = joomla_manifest.find_manifest(src_dir)
|
ext_info = extension_utils.get_extension_info(src_dir)
|
||||||
manifest_info = joomla_manifest.parse_manifest(manifest_path)
|
|
||||||
|
if not ext_info:
|
||||||
|
common.die(f"No Joomla or Dolibarr extension found in {src_dir}")
|
||||||
|
|
||||||
# Determine version
|
# Determine version
|
||||||
if not version:
|
if not version:
|
||||||
version = manifest_info.version
|
version = ext_info.version
|
||||||
|
|
||||||
# Determine repo name
|
# Determine repo name
|
||||||
if not repo_name:
|
if not repo_name:
|
||||||
@@ -163,7 +165,8 @@ def create_package(
|
|||||||
|
|
||||||
# Generate ZIP filename
|
# Generate ZIP filename
|
||||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
zip_filename = f"{repo_name}-{version}-{manifest_info.extension_type}.zip"
|
platform_suffix = f"{ext_info.platform.value}-{ext_info.extension_type}"
|
||||||
|
zip_filename = f"{repo_name}-{version}-{platform_suffix}.zip"
|
||||||
zip_path = output_path / zip_filename
|
zip_path = output_path / zip_filename
|
||||||
|
|
||||||
# Remove existing ZIP if present
|
# Remove existing ZIP if present
|
||||||
@@ -171,8 +174,9 @@ def create_package(
|
|||||||
zip_path.unlink()
|
zip_path.unlink()
|
||||||
|
|
||||||
common.log_section("Creating Extension Package")
|
common.log_section("Creating Extension Package")
|
||||||
common.log_kv("Extension", manifest_info.name)
|
common.log_kv("Platform", ext_info.platform.value.upper())
|
||||||
common.log_kv("Type", manifest_info.extension_type)
|
common.log_kv("Extension", ext_info.name)
|
||||||
|
common.log_kv("Type", ext_info.extension_type)
|
||||||
common.log_kv("Version", version)
|
common.log_kv("Version", version)
|
||||||
common.log_kv("Source", src_dir)
|
common.log_kv("Source", src_dir)
|
||||||
common.log_kv("Output", str(zip_path))
|
common.log_kv("Output", str(zip_path))
|
||||||
@@ -207,8 +211,9 @@ def create_package(
|
|||||||
# Output JSON for machine consumption
|
# Output JSON for machine consumption
|
||||||
result = {
|
result = {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"extension": manifest_info.name,
|
"platform": ext_info.platform.value,
|
||||||
"ext_type": manifest_info.extension_type,
|
"extension": ext_info.name,
|
||||||
|
"ext_type": ext_info.extension_type,
|
||||||
"version": version,
|
"version": version,
|
||||||
"package": str(zip_path),
|
"package": str(zip_path),
|
||||||
|
The reference to The reference to `ext_info.platform` will fail because the `extension_utils` module that defines the `ExtensionInfo` class and `platform` attribute is missing from this PR.
|
|||||||
"files": file_count,
|
"files": file_count,
|
||||||
@@ -224,7 +229,7 @@ def create_package(
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Package Joomla extension as distributable ZIP",
|
description="Package Joomla or Dolibarr extension as distributable ZIP",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
@@ -239,6 +244,8 @@ Examples:
|
|||||||
|
|
||||||
# Package with custom source
|
# Package with custom source
|
||||||
%(prog)s --src-dir my-extension dist 1.0.0
|
%(prog)s --src-dir my-extension dist 1.0.0
|
||||||
|
|
||||||
|
Supports both Joomla and Dolibarr extensions with automatic platform detection.
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Package Joomla extension as distributable ZIP
|
|
||||||
# USAGE: ./scripts/release/package_extension.sh [output_dir] [version]
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Load shared library functions (optional)
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
LIB_DIR="${SCRIPT_DIR}/../lib"
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
OUTPUT_DIR="${1:-dist}"
|
|
||||||
VERSION="${2:-}"
|
|
||||||
REPO_NAME="${REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}"
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
echo "[INFO] $*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo "[ERROR] $*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate prerequisites
|
|
||||||
validate_prerequisites() {
|
|
||||||
if [ ! -d "${SRC_DIR}" ]; then
|
|
||||||
log_error "Source directory '${SRC_DIR}' not found"
|
|
||||||
printf '{"status":"fail","error":%s}\n' "$(json_escape "src directory missing")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v zip >/dev/null 2>&1; then
|
|
||||||
log_error "zip command not found. Please install zip utility."
|
|
||||||
printf '{"status":"fail","error":%s}\n' "$(json_escape "zip command not found")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find and validate manifest
|
|
||||||
find_manifest_file() {
|
|
||||||
local manifest=""
|
|
||||||
|
|
||||||
# Priority order for finding manifest
|
|
||||||
if [ -f "${SRC_DIR}/templateDetails.xml" ]; then
|
|
||||||
manifest="${SRC_DIR}/templateDetails.xml"
|
|
||||||
elif [ -f "${SRC_DIR}/templates/templateDetails.xml" ]; then
|
|
||||||
manifest="${SRC_DIR}/templates/templateDetails.xml"
|
|
||||||
else
|
|
||||||
# Try finding any Joomla manifest
|
|
||||||
manifest=$(find "${SRC_DIR}" -maxdepth 3 -type f \( \
|
|
||||||
-name 'templateDetails.xml' -o \
|
|
||||||
-name 'pkg_*.xml' -o \
|
|
||||||
-name 'mod_*.xml' -o \
|
|
||||||
-name 'com_*.xml' -o \
|
|
||||||
-name 'plg_*.xml' \
|
|
||||||
\) | head -n 1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${manifest}" ]; then
|
|
||||||
log_error "No Joomla manifest XML found in ${SRC_DIR}"
|
|
||||||
printf '{"status":"fail","error":%s}\n' "$(json_escape "manifest not found")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${manifest}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract extension metadata from manifest
|
|
||||||
get_extension_metadata() {
|
|
||||||
local manifest="$1"
|
|
||||||
local ext_type=""
|
|
||||||
local ext_name=""
|
|
||||||
local ext_version=""
|
|
||||||
|
|
||||||
# Extract extension type
|
|
||||||
ext_type=$(grep -Eo 'type="[^"]+"' "${manifest}" | head -n 1 | cut -d '"' -f2 || echo "unknown")
|
|
||||||
|
|
||||||
# Extract extension name
|
|
||||||
ext_name=$(grep -oP '<name>\K[^<]+' "${manifest}" | head -n 1 || echo "unknown")
|
|
||||||
|
|
||||||
# Extract version
|
|
||||||
ext_version=$(grep -oP '<version>\K[^<]+' "${manifest}" | head -n 1 || echo "unknown")
|
|
||||||
|
|
||||||
echo "${ext_type}|${ext_name}|${ext_version}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create package
|
|
||||||
create_package() {
|
|
||||||
local manifest="$1"
|
|
||||||
local output_dir="$2"
|
|
||||||
local version="$3"
|
|
||||||
|
|
||||||
# Get extension metadata
|
|
||||||
local metadata
|
|
||||||
metadata=$(get_extension_metadata "${manifest}")
|
|
||||||
local ext_type=$(echo "${metadata}" | cut -d '|' -f1)
|
|
||||||
local ext_name=$(echo "${metadata}" | cut -d '|' -f2)
|
|
||||||
local manifest_version=$(echo "${metadata}" | cut -d '|' -f3)
|
|
||||||
|
|
||||||
# Use provided version or fall back to manifest version
|
|
||||||
if [ -z "${version}" ]; then
|
|
||||||
version="${manifest_version}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create output directory
|
|
||||||
mkdir -p "${output_dir}"
|
|
||||||
|
|
||||||
# Generate package filename
|
|
||||||
local timestamp=$(date +%Y%m%d-%H%M%S)
|
|
||||||
local zip_name="${REPO_NAME}-${version}-${ext_type}.zip"
|
|
||||||
|
|
||||||
# Get absolute path for zip file
|
|
||||||
local abs_output_dir
|
|
||||||
if [[ "${output_dir}" = /* ]]; then
|
|
||||||
abs_output_dir="${output_dir}"
|
|
||||||
else
|
|
||||||
abs_output_dir="$(pwd)/${output_dir}"
|
|
||||||
fi
|
|
||||||
local zip_path="${abs_output_dir}/${zip_name}"
|
|
||||||
|
|
||||||
log_info "Creating package: ${zip_name}"
|
|
||||||
log_info "Extension: ${ext_name} (${ext_type})"
|
|
||||||
log_info "Version: ${version}"
|
|
||||||
|
|
||||||
# Create ZIP archive excluding unnecessary files
|
|
||||||
(cd "${SRC_DIR}" && zip -r -q -X "${zip_path}" . \
|
|
||||||
-x '*.git*' \
|
|
||||||
-x '*/.github/*' \
|
|
||||||
-x '*.DS_Store' \
|
|
||||||
-x '*/__MACOSX/*' \
|
|
||||||
-x '*/node_modules/*' \
|
|
||||||
-x '*/vendor/*' \
|
|
||||||
-x '*/tests/*' \
|
|
||||||
-x '*/.phpunit.result.cache' \
|
|
||||||
-x '*/codeception.yml' \
|
|
||||||
-x '*/composer.json' \
|
|
||||||
-x '*/composer.lock' \
|
|
||||||
-x '*/package.json' \
|
|
||||||
-x '*/package-lock.json')
|
|
||||||
|
|
||||||
# Get file size
|
|
||||||
local zip_size
|
|
||||||
if command -v stat >/dev/null 2>&1; then
|
|
||||||
zip_size=$(stat -f%z "${zip_path}" 2>/dev/null || stat -c%s "${zip_path}" 2>/dev/null || echo "unknown")
|
|
||||||
else
|
|
||||||
zip_size="unknown"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Package created successfully: ${zip_path}"
|
|
||||||
log_info "Package size: ${zip_size} bytes"
|
|
||||||
|
|
||||||
# Output JSON result
|
|
||||||
printf '{"status":"ok","package":%s,"type":%s,"version":%s,"size":%s,"manifest":%s}\n' \
|
|
||||||
"$(json_escape "${zip_path}")" \
|
|
||||||
"$(json_escape "${ext_type}")" \
|
|
||||||
"$(json_escape "${version}")" \
|
|
||||||
"${zip_size}" \
|
|
||||||
"$(json_escape "${manifest}")"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
main() {
|
|
||||||
log_info "Starting Joomla extension packaging"
|
|
||||||
|
|
||||||
validate_prerequisites
|
|
||||||
|
|
||||||
local manifest
|
|
||||||
manifest=$(find_manifest_file)
|
|
||||||
log_info "Using manifest: ${manifest}"
|
|
||||||
|
|
||||||
create_package "${manifest}" "${OUTPUT_DIR}" "${VERSION}"
|
|
||||||
|
|
||||||
log_info "Packaging completed successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run main function
|
|
||||||
main "$@"
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 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 (./LICENSE).
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoStandards
|
|
||||||
# INGROUP: Generic.Script
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /scripts/update_changelog.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Insert a versioned CHANGELOG.md entry immediately after the main Changelog heading
|
|
||||||
# Purpose:
|
|
||||||
# - Apply the MokoWaaS-Brand CHANGELOG template entry for a given version.
|
|
||||||
# - Insert a new header at the top of CHANGELOG.md, immediately after "# Changelog".
|
|
||||||
# - Avoid duplicates if an entry for the version already exists.
|
|
||||||
# - Preserve the rest of the file verbatim.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/update_changelog.sh <VERSION>
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# ./scripts/update_changelog.sh 01.05.00
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
CHANGELOG_FILE="CHANGELOG.md"
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "ERROR: $*" 1>&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
info() {
|
|
||||||
echo "INFO: $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
validate/version() {
|
|
||||||
local v="$1"
|
|
||||||
[[ "$v" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]] || die "Invalid version '$v'. Expected NN.NN.NN (example 03.01.00)."
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
require_cmd awk
|
|
||||||
require_cmd grep
|
|
||||||
require_cmd mktemp
|
|
||||||
require_cmd date
|
|
||||||
|
|
||||||
[[ $# -eq 1 ]] || die "Usage: $0 <VERSION>"
|
|
||||||
local version="$1"
|
|
||||||
validate/version "$version"
|
|
||||||
|
|
||||||
[[ -f "$CHANGELOG_FILE" ]] || die "Missing $CHANGELOG_FILE in repo root."
|
|
||||||
|
|
||||||
if ! grep -qE '^# Changelog[[:space:]]*$' "$CHANGELOG_FILE"; then
|
|
||||||
die "$CHANGELOG_FILE must contain a top level heading exactly: # Changelog"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qE "^## \[$version\][[:space:]]" "$CHANGELOG_FILE"; then
|
|
||||||
info "CHANGELOG.md already contains an entry for version $version. No action taken."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local stamp
|
|
||||||
stamp="$(date '+%Y-%m-%d')"
|
|
||||||
|
|
||||||
local tmp
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
trap 'rm -f "$tmp"' EXIT
|
|
||||||
|
|
||||||
awk -v v="$version" -v d="$stamp" '
|
|
||||||
BEGIN { inserted=0 }
|
|
||||||
{
|
|
||||||
print $0
|
|
||||||
if (inserted==0 && $0 ~ /^# Changelog[[:space:]]*$/) {
|
|
||||||
print ""
|
|
||||||
print "## [" v "] " d
|
|
||||||
print "- Version bump."
|
|
||||||
print ""
|
|
||||||
inserted=1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END {
|
|
||||||
if (inserted==0) {
|
|
||||||
exit 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$CHANGELOG_FILE" > "$tmp" || {
|
|
||||||
rc=$?
|
|
||||||
if [[ $rc -eq 3 ]]; then
|
|
||||||
die "Insertion point not found. Expected: # Changelog"
|
|
||||||
fi
|
|
||||||
die "Failed to update $CHANGELOG_FILE (awk exit code $rc)."
|
|
||||||
}
|
|
||||||
|
|
||||||
mv "$tmp" "$CHANGELOG_FILE"
|
|
||||||
trap - EXIT
|
|
||||||
|
|
||||||
info "Inserted CHANGELOG.md entry for version $version on $stamp."
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# 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. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Release.Automation
|
|
||||||
# INGROUP: Date.Normalization
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
|
||||||
# PATH: /scripts/release/update_dates.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Normalize release dates across manifests and CHANGELOG using a single authoritative UTC date.
|
|
||||||
# NOTE: Repo-controlled script only. CI-fatal on malformed inputs. Outputs a JSON report to stdout.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TODAY_UTC="${1:-}"
|
|
||||||
VERSION="${2:-}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "ERROR: Usage: update_dates.sh <YYYY-MM-DD> <VERSION>" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -z "${TODAY_UTC}" ] || [ -z "${VERSION}" ]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate date format strictly
|
|
||||||
if ! echo "${TODAY_UTC}" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then
|
|
||||||
echo "ERROR: Invalid date format. Expected YYYY-MM-DD, got '${TODAY_UTC}'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate version format strictly
|
|
||||||
if ! echo "${VERSION}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
echo "ERROR: Invalid version format. Expected X.Y.Z, got '${VERSION}'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cross-platform sed in-place helper (GNU and BSD)
|
|
||||||
# - Ubuntu runners use GNU sed, but this keeps local execution deterministic.
|
|
||||||
sed_inplace() {
|
|
||||||
local expr="$1"
|
|
||||||
local file="$2"
|
|
||||||
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i -E "${expr}" "${file}"
|
|
||||||
else
|
|
||||||
sed -i '' -E "${expr}" "${file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Normalizing dates to ${TODAY_UTC} for version ${VERSION}"
|
|
||||||
|
|
||||||
# Update CHANGELOG.md heading date
|
|
||||||
if [ ! -f CHANGELOG.md ]; then
|
|
||||||
echo "ERROR: CHANGELOG.md not found" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -Eq "^## \[${VERSION}\]" CHANGELOG.md; then
|
|
||||||
echo "ERROR: CHANGELOG.md does not contain heading for version [${VERSION}]" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use a delimiter that will not collide with the pattern (the heading starts with "##")
|
|
||||||
sed_inplace "s|^(## \[${VERSION}\]) .*|\1 ${TODAY_UTC}|" CHANGELOG.md
|
|
||||||
|
|
||||||
# Update XML manifest dates
|
|
||||||
XML_SCANNED=0
|
|
||||||
XML_TOUCHED=0
|
|
||||||
|
|
||||||
while IFS= read -r -d '' FILE; do
|
|
||||||
XML_SCANNED=$((XML_SCANNED + 1))
|
|
||||||
|
|
||||||
BEFORE_HASH=""
|
|
||||||
AFTER_HASH=""
|
|
||||||
|
|
||||||
# Best-effort content hash for change detection without external deps.
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then
|
|
||||||
BEFORE_HASH="$(sha256sum "${FILE}" | awk '{print $1}')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use # delimiter because XML does not include # in these tags.
|
|
||||||
sed -i "s#<creationDate>[^<]*</creationDate>#<creationDate>${TODAY_UTC}</creationDate>#g" "${FILE}" || true
|
|
||||||
sed -i "s#<date>[^<]*</date>#<date>${TODAY_UTC}</date>#g" "${FILE}" || true
|
|
||||||
sed -i "s#<buildDate>[^<]*</buildDate>#<buildDate>${TODAY_UTC}</buildDate>#g" "${FILE}" || true
|
|
||||||
|
|
||||||
if [ -n "${BEFORE_HASH}" ]; then
|
|
||||||
AFTER_HASH="$(sha256sum "${FILE}" | awk '{print $1}')"
|
|
||||||
if [ "${BEFORE_HASH}" != "${AFTER_HASH}" ]; then
|
|
||||||
XML_TOUCHED=$((XML_TOUCHED + 1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
done < <(
|
|
||||||
find . -type f -name "*.xml" \
|
|
||||||
-not -path "./.git/*" \
|
|
||||||
-not -path "./.github/*" \
|
|
||||||
-not -path "./dist/*" \
|
|
||||||
-not -path "./node_modules/*" \
|
|
||||||
-print0
|
|
||||||
)
|
|
||||||
|
|
||||||
# JSON report to stdout (workflow can capture or include in summary)
|
|
||||||
printf '{"today_utc":"%s","version":"%s","changelog":"%s","xml_scanned":%s,"xml_touched":%s}
|
|
||||||
' \
|
|
||||||
"${TODAY_UTC}" \
|
|
||||||
"${VERSION}" \
|
|
||||||
"CHANGELOG.md" \
|
|
||||||
"${XML_SCANNED}" \
|
|
||||||
"${XML_TOUCHED}"
|
|
||||||
|
|
||||||
echo "Date normalization complete."
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.Run
|
|
||||||
# INGROUP: Version.Management
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/run/check_version.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Check if a version can be created in a specific branch prefix
|
|
||||||
# NOTE: Validates against version hierarchy rules before creation
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Usage and validation
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<-USAGE
|
|
||||||
Usage: $0 <BRANCH_PREFIX> <VERSION>
|
|
||||||
|
|
||||||
Check if a version can be created in the specified branch prefix.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
BRANCH_PREFIX One of: dev/, rc/, version/
|
|
||||||
VERSION Version in format NN.NN.NN (e.g., 03.01.00)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$0 dev/ 03.05.00
|
|
||||||
$0 rc/ 03.01.00
|
|
||||||
$0 version/ 02.00.00
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
0 - Version can be created (no conflicts)
|
|
||||||
1 - Version cannot be created (conflicts found)
|
|
||||||
2 - Invalid arguments or usage
|
|
||||||
|
|
||||||
Version Hierarchy Rules:
|
|
||||||
- version/X.Y.Z (stable) - always allowed, highest priority
|
|
||||||
- rc/X.Y.Z (RC) - blocked if version/X.Y.Z exists
|
|
||||||
- dev/X.Y.Z (dev) - blocked if version/X.Y.Z or rc/X.Y.Z exists
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_prefix() {
|
|
||||||
local prefix="$1"
|
|
||||||
case "$prefix" in
|
|
||||||
"dev/"|"rc/"|"version/")
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "Invalid branch prefix: $prefix (must be dev/, rc/, or version/)"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_version() {
|
|
||||||
local v="$1"
|
|
||||||
if [[ ! "$v" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]]; then
|
|
||||||
die "Invalid version format: $v (expected NN.NN.NN)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main logic
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
check_version_can_be_created() {
|
|
||||||
local prefix="$1"
|
|
||||||
local version="$2"
|
|
||||||
|
|
||||||
log_info "Checking if ${prefix}${version} can be created..."
|
|
||||||
log_info ""
|
|
||||||
|
|
||||||
# Check if the exact branch already exists
|
|
||||||
if git ls-remote --exit-code --heads origin "${prefix}${version}" >/dev/null 2>&1; then
|
|
||||||
log_error "✗ Branch already exists: ${prefix}${version}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local conflicts=0
|
|
||||||
|
|
||||||
case "$prefix" in
|
|
||||||
"version/")
|
|
||||||
log_info "Creating stable version - no hierarchy checks needed"
|
|
||||||
log_info "✓ version/${version} can be created"
|
|
||||||
;;
|
|
||||||
"rc/")
|
|
||||||
log_info "Checking if version exists in stable..."
|
|
||||||
if git ls-remote --exit-code --heads origin "version/${version}" >/dev/null 2>&1; then
|
|
||||||
log_error "✗ CONFLICT: Version ${version} already exists in stable (version/${version})"
|
|
||||||
log_error " Cannot create RC for a version that exists in stable"
|
|
||||||
conflicts=$((conflicts + 1))
|
|
||||||
else
|
|
||||||
log_info "✓ No conflict with stable versions"
|
|
||||||
log_info "✓ rc/${version} can be created"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"dev/")
|
|
||||||
log_info "Checking if version exists in stable..."
|
|
||||||
if git ls-remote --exit-code --heads origin "version/${version}" >/dev/null 2>&1; then
|
|
||||||
log_error "✗ CONFLICT: Version ${version} already exists in stable (version/${version})"
|
|
||||||
log_error " Cannot create dev for a version that exists in stable"
|
|
||||||
conflicts=$((conflicts + 1))
|
|
||||||
else
|
|
||||||
log_info "✓ No conflict with stable versions"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Checking if version exists in RC..."
|
|
||||||
if git ls-remote --exit-code --heads origin "rc/${version}" >/dev/null 2>&1; then
|
|
||||||
log_error "✗ CONFLICT: Version ${version} already exists in RC (rc/${version})"
|
|
||||||
log_error " Cannot create dev for a version that exists in RC"
|
|
||||||
conflicts=$((conflicts + 1))
|
|
||||||
else
|
|
||||||
log_info "✓ No conflict with RC versions"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $conflicts -eq 0 ]; then
|
|
||||||
log_info "✓ dev/${version} can be created"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
log_info ""
|
|
||||||
|
|
||||||
if [ $conflicts -gt 0 ]; then
|
|
||||||
log_error "Version ${prefix}${version} cannot be created ($conflicts conflict(s) found)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Version ${prefix}${version} can be created safely"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ $# -eq 2 ] || usage
|
|
||||||
|
|
||||||
BRANCH_PREFIX="$1"
|
|
||||||
VERSION="$2"
|
|
||||||
|
|
||||||
validate_prefix "$BRANCH_PREFIX"
|
|
||||||
validate_version "$VERSION"
|
|
||||||
|
|
||||||
log_info "Version Creation Check"
|
|
||||||
log_info "======================"
|
|
||||||
log_info ""
|
|
||||||
|
|
||||||
check_version_can_be_created "$BRANCH_PREFIX" "$VERSION"
|
|
||||||
exit_code=$?
|
|
||||||
|
|
||||||
log_info ""
|
|
||||||
log_info "======================"
|
|
||||||
|
|
||||||
exit $exit_code
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.Run
|
|
||||||
# INGROUP: Version.Management
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/run/list_versions.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: List all version branches organized by prefix
|
|
||||||
# NOTE: Displays dev/, rc/, and version/ branches in a structured format
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Functions
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
list_version_branches() {
|
|
||||||
log_info "Fetching version branches from remote..."
|
|
||||||
|
|
||||||
# Get all branches with version-like names
|
|
||||||
local branches
|
|
||||||
branches=$(git ls-remote --heads origin 2>/dev/null | awk '{print $2}' | sed 's|refs/heads/||' || echo "")
|
|
||||||
|
|
||||||
if [ -z "$branches" ]; then
|
|
||||||
log_warn "No remote branches found or unable to fetch branches"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Categorize versions
|
|
||||||
local dev_versions=()
|
|
||||||
local rc_versions=()
|
|
||||||
local stable_versions=()
|
|
||||||
|
|
||||||
while IFS= read -r branch; do
|
|
||||||
if [[ "$branch" =~ ^dev/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then
|
|
||||||
dev_versions+=("${BASH_REMATCH[1]}")
|
|
||||||
elif [[ "$branch" =~ ^rc/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then
|
|
||||||
rc_versions+=("${BASH_REMATCH[1]}")
|
|
||||||
elif [[ "$branch" =~ ^version/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then
|
|
||||||
stable_versions+=("${BASH_REMATCH[1]}")
|
|
||||||
fi
|
|
||||||
done <<< "$branches"
|
|
||||||
|
|
||||||
# Sort versions
|
|
||||||
IFS=$'\n' dev_versions=($(sort -V <<< "${dev_versions[*]}" 2>/dev/null || echo "${dev_versions[@]}"))
|
|
||||||
IFS=$'\n' rc_versions=($(sort -V <<< "${rc_versions[*]}" 2>/dev/null || echo "${rc_versions[@]}"))
|
|
||||||
IFS=$'\n' stable_versions=($(sort -V <<< "${stable_versions[*]}" 2>/dev/null || echo "${stable_versions[@]}"))
|
|
||||||
unset IFS
|
|
||||||
|
|
||||||
# Display results
|
|
||||||
echo ""
|
|
||||||
echo "========================================"
|
|
||||||
echo "Version Branches Summary"
|
|
||||||
echo "========================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "📦 Stable Versions (version/)"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
if [ ${#stable_versions[@]} -eq 0 ]; then
|
|
||||||
echo " (none)"
|
|
||||||
else
|
|
||||||
for version in "${stable_versions[@]}"; do
|
|
||||||
echo " ✓ version/$version"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "🔧 Release Candidates (rc/)"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
if [ ${#rc_versions[@]} -eq 0 ]; then
|
|
||||||
echo " (none)"
|
|
||||||
else
|
|
||||||
for version in "${rc_versions[@]}"; do
|
|
||||||
echo " ➜ rc/$version"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "🚧 Development Versions (dev/)"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
if [ ${#dev_versions[@]} -eq 0 ]; then
|
|
||||||
echo " (none)"
|
|
||||||
else
|
|
||||||
for version in "${dev_versions[@]}"; do
|
|
||||||
echo " ⚡ dev/$version"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo "Total: ${#stable_versions[@]} stable, ${#rc_versions[@]} RC, ${#dev_versions[@]} dev"
|
|
||||||
echo "========================================"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
list_version_branches
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.Run
|
|
||||||
# INGROUP: Repository.Release
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/run/migrate_unreleased.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Migrate unreleased changelog entries to a versioned section
|
|
||||||
# NOTE: Moves content from [Unreleased] section to a specified version heading
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Usage
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<-USAGE
|
|
||||||
Usage: $0 <VERSION> [OPTIONS]
|
|
||||||
|
|
||||||
Migrate unreleased changelog entries to a versioned section.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
VERSION Version number in format NN.NN.NN (e.g., 03.05.00)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help Show this help message
|
|
||||||
-d, --date Date to use for version entry (default: today, format: YYYY-MM-DD)
|
|
||||||
-n, --dry-run Show what would be done without making changes
|
|
||||||
-k, --keep Keep the [Unreleased] section after migration (default: empty it)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$0 03.05.00 # Migrate unreleased to version 03.05.00
|
|
||||||
$0 03.05.00 --date 2026-01-04 # Use specific date
|
|
||||||
$0 03.05.00 --dry-run # Preview changes without applying
|
|
||||||
$0 03.05.00 --keep # Keep unreleased section after migration
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Argument parsing
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
VERSION=""
|
|
||||||
DATE=""
|
|
||||||
DRY_RUN=false
|
|
||||||
KEEP_UNRELEASED=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
-d|--date)
|
|
||||||
DATE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-n|--dry-run)
|
|
||||||
DRY_RUN=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-k|--keep)
|
|
||||||
KEEP_UNRELEASED=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [[ -z "$VERSION" ]]; then
|
|
||||||
VERSION="$1"
|
|
||||||
shift
|
|
||||||
else
|
|
||||||
echo "ERROR: Unknown argument: $1" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Validation
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if [[ -z "$VERSION" ]]; then
|
|
||||||
echo "ERROR: VERSION is required" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! [[ "$VERSION" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]]; then
|
|
||||||
echo "ERROR: Invalid version format: $VERSION" >&2
|
|
||||||
echo "Expected format: NN.NN.NN (e.g., 03.05.00)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$DATE" ]]; then
|
|
||||||
DATE=$(date '+%Y-%m-%d')
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! [[ "$DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
|
||||||
echo "ERROR: Invalid date format: $DATE" >&2
|
|
||||||
echo "Expected format: YYYY-MM-DD" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Source common utilities
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main logic
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
CHANGELOG_FILE="CHANGELOG.md"
|
|
||||||
|
|
||||||
if [[ ! -f "$CHANGELOG_FILE" ]]; then
|
|
||||||
log_error "CHANGELOG.md not found in repository root"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Migrating unreleased changelog entries to version $VERSION"
|
|
||||||
log_info "Date: $DATE"
|
|
||||||
log_info "Dry run: $DRY_RUN"
|
|
||||||
log_info "Keep unreleased section: $KEEP_UNRELEASED"
|
|
||||||
|
|
||||||
# Use Python to process the changelog
|
|
||||||
python3 - <<PY
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
version = "${VERSION}"
|
|
||||||
stamp = "${DATE}"
|
|
||||||
dry_run = "${DRY_RUN}" == "true"
|
|
||||||
keep_unreleased = "${KEEP_UNRELEASED}" == "true"
|
|
||||||
|
|
||||||
changelog_path = Path("${CHANGELOG_FILE}")
|
|
||||||
lines = changelog_path.read_text(encoding="utf-8", errors="replace").splitlines(True)
|
|
||||||
|
|
||||||
def is_h2(line: str) -> bool:
|
|
||||||
return line.lstrip().startswith("## ")
|
|
||||||
|
|
||||||
def norm(line: str) -> str:
|
|
||||||
return line.strip().lower()
|
|
||||||
|
|
||||||
def find_idx(predicate):
|
|
||||||
for i, ln in enumerate(lines):
|
|
||||||
if predicate(ln):
|
|
||||||
return i
|
|
||||||
return None
|
|
||||||
|
|
||||||
unreleased_idx = find_idx(lambda ln: norm(ln) == "## [unreleased]")
|
|
||||||
version_idx = find_idx(lambda ln: ln.lstrip().startswith(f"## [{version}]"))
|
|
||||||
|
|
||||||
def version_header() -> list:
|
|
||||||
return ["\n", f"## [{version}] {stamp}\n", "\n"]
|
|
||||||
|
|
||||||
if unreleased_idx is None:
|
|
||||||
print(f"INFO: No [Unreleased] section found in {changelog_path}")
|
|
||||||
if version_idx is None:
|
|
||||||
print(f"INFO: Version section [{version}] does not exist")
|
|
||||||
print(f"INFO: Creating new version section with placeholder content")
|
|
||||||
if not dry_run:
|
|
||||||
# Find insertion point after main heading
|
|
||||||
insert_at = 0
|
|
||||||
for i, ln in enumerate(lines):
|
|
||||||
if ln.lstrip().startswith("# "):
|
|
||||||
insert_at = i + 1
|
|
||||||
while insert_at < len(lines) and lines[insert_at].strip() == "":
|
|
||||||
insert_at += 1
|
|
||||||
break
|
|
||||||
entry = version_header() + ["- No changes recorded.\n", "\n"]
|
|
||||||
lines[insert_at:insert_at] = entry
|
|
||||||
changelog_path.write_text("".join(lines), encoding="utf-8")
|
|
||||||
print(f"SUCCESS: Created version section [{version}]")
|
|
||||||
else:
|
|
||||||
print(f"DRY-RUN: Would create version section [{version}]")
|
|
||||||
else:
|
|
||||||
print(f"INFO: Version section [{version}] already exists")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Extract unreleased content
|
|
||||||
u_start = unreleased_idx + 1
|
|
||||||
u_end = len(lines)
|
|
||||||
for j in range(u_start, len(lines)):
|
|
||||||
if is_h2(lines[j]):
|
|
||||||
u_end = j
|
|
||||||
break
|
|
||||||
|
|
||||||
unreleased_body = "".join(lines[u_start:u_end]).strip()
|
|
||||||
|
|
||||||
if not unreleased_body:
|
|
||||||
print(f"INFO: [Unreleased] section is empty, nothing to migrate")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
print(f"INFO: Found unreleased content ({len(unreleased_body)} chars)")
|
|
||||||
|
|
||||||
# Create or find version section
|
|
||||||
if version_idx is None:
|
|
||||||
print(f"INFO: Creating version section [{version}]")
|
|
||||||
if not dry_run:
|
|
||||||
lines[u_end:u_end] = version_header()
|
|
||||||
else:
|
|
||||||
print(f"DRY-RUN: Would create version section [{version}]")
|
|
||||||
|
|
||||||
version_idx = find_idx(lambda ln: ln.lstrip().startswith(f"## [{version}]"))
|
|
||||||
if version_idx is None and not dry_run:
|
|
||||||
print("ERROR: Failed to locate version header after insertion", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Move unreleased content to version section
|
|
||||||
if unreleased_body:
|
|
||||||
if not dry_run:
|
|
||||||
insert_at = version_idx + 1
|
|
||||||
while insert_at < len(lines) and lines[insert_at].strip() == "":
|
|
||||||
insert_at += 1
|
|
||||||
|
|
||||||
moved = ["\n"] + [ln + "\n" for ln in unreleased_body.split("\n") if ln != ""] + ["\n"]
|
|
||||||
lines[insert_at:insert_at] = moved
|
|
||||||
print(f"INFO: Moved {len([ln for ln in unreleased_body.split('\n') if ln])} lines to [{version}]")
|
|
||||||
else:
|
|
||||||
line_count = len([ln for ln in unreleased_body.split('\n') if ln])
|
|
||||||
print(f"DRY-RUN: Would move {line_count} lines to [{version}]")
|
|
||||||
print(f"DRY-RUN: Content preview:")
|
|
||||||
for line in unreleased_body.split('\n')[:5]:
|
|
||||||
if line:
|
|
||||||
print(f" {line}")
|
|
||||||
|
|
||||||
# Handle unreleased section
|
|
||||||
if not keep_unreleased:
|
|
||||||
unreleased_idx = find_idx(lambda ln: norm(ln) == "## [unreleased]")
|
|
||||||
if unreleased_idx is not None:
|
|
||||||
if not dry_run:
|
|
||||||
u_start = unreleased_idx + 1
|
|
||||||
u_end = len(lines)
|
|
||||||
for j in range(u_start, len(lines)):
|
|
||||||
if is_h2(lines[j]):
|
|
||||||
u_end = j
|
|
||||||
break
|
|
||||||
lines[u_start:u_end] = ["\n"]
|
|
||||||
print(f"INFO: Emptied [Unreleased] section")
|
|
||||||
else:
|
|
||||||
print(f"DRY-RUN: Would empty [Unreleased] section")
|
|
||||||
else:
|
|
||||||
print(f"INFO: Keeping [Unreleased] section as requested")
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
changelog_path.write_text("".join(lines), encoding="utf-8")
|
|
||||||
print(f"SUCCESS: Migrated unreleased content to [{version}]")
|
|
||||||
else:
|
|
||||||
print(f"DRY-RUN: Changes not applied (use without --dry-run to apply)")
|
|
||||||
|
|
||||||
PY
|
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
if [[ "$DRY_RUN" == "false" ]]; then
|
|
||||||
log_info "✓ Migration completed successfully"
|
|
||||||
log_info "✓ Changelog updated: $CHANGELOG_FILE"
|
|
||||||
else
|
|
||||||
log_info "✓ Dry run completed"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_error "Migration failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Script.Health
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/run/script_health.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validate scripts follow enterprise standards
|
|
||||||
# NOTE: Checks for copyright headers, error handling, and documentation
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
. "${SCRIPT_DIR}/lib/logging.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Usage
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<-USAGE
|
|
||||||
Usage: $0 [OPTIONS]
|
|
||||||
|
|
||||||
Validate that all scripts follow enterprise standards.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-v, --verbose Show detailed output
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
Checks performed:
|
|
||||||
- Copyright headers present
|
|
||||||
- SPDX license identifier present
|
|
||||||
- FILE INFORMATION section present
|
|
||||||
- set -euo pipefail present
|
|
||||||
- Executable permissions set
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$0 # Run all health checks
|
|
||||||
$0 -v # Verbose output
|
|
||||||
$0 --help # Show usage
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
0 - All checks passed
|
|
||||||
1 - One or more checks failed
|
|
||||||
2 - Invalid arguments
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Configuration
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
VERBOSE="${1:-}"
|
|
||||||
|
|
||||||
case "${VERBOSE}" in
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
-v|--verbose)
|
|
||||||
VERBOSE="true"
|
|
||||||
;;
|
|
||||||
"")
|
|
||||||
VERBOSE="false"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "Invalid argument: ${VERBOSE}"
|
|
||||||
echo ""
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
log_info "Running script health checks"
|
|
||||||
log_info "Start time: $(log_timestamp)"
|
|
||||||
|
|
||||||
START_TIME=$(date +%s)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Health checks
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
total_scripts=0
|
|
||||||
missing_copyright=0
|
|
||||||
missing_spdx=0
|
|
||||||
missing_fileinfo=0
|
|
||||||
missing_error_handling=0
|
|
||||||
not_executable=0
|
|
||||||
|
|
||||||
check_script() {
|
|
||||||
local script="$1"
|
|
||||||
local errors=0
|
|
||||||
|
|
||||||
total_scripts=$((total_scripts + 1))
|
|
||||||
|
|
||||||
# Check for copyright
|
|
||||||
if ! grep -q "Copyright (C)" "$script"; then
|
|
||||||
missing_copyright=$((missing_copyright + 1))
|
|
||||||
errors=$((errors + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && log_warn "Missing copyright: $script"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for SPDX
|
|
||||||
if ! grep -q "SPDX-License-Identifier" "$script"; then
|
|
||||||
missing_spdx=$((missing_spdx + 1))
|
|
||||||
errors=$((errors + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && log_warn "Missing SPDX: $script"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for FILE INFORMATION
|
|
||||||
if ! grep -q "FILE INFORMATION" "$script"; then
|
|
||||||
missing_fileinfo=$((missing_fileinfo + 1))
|
|
||||||
errors=$((errors + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && log_warn "Missing FILE INFORMATION: $script"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for error handling (bash scripts only)
|
|
||||||
if [[ "$script" == *.sh ]]; then
|
|
||||||
if ! grep -q "set -e" "$script"; then
|
|
||||||
missing_error_handling=$((missing_error_handling + 1))
|
|
||||||
errors=$((errors + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && log_warn "Missing error handling: $script"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check executable permission
|
|
||||||
if [ ! -x "$script" ]; then
|
|
||||||
not_executable=$((not_executable + 1))
|
|
||||||
errors=$((errors + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && log_warn "Not executable: $script"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return $errors
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find all shell scripts
|
|
||||||
log_info "Scanning scripts directory..."
|
|
||||||
|
|
||||||
while IFS= read -r -d '' script; do
|
|
||||||
check_script "$script" || true
|
|
||||||
done < <(find "${SCRIPT_DIR}" -type f -name "*.sh" -print0)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Summary
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
END_TIME=$(date +%s)
|
|
||||||
|
|
||||||
log_separator
|
|
||||||
log_info "Script Health Summary"
|
|
||||||
log_separator
|
|
||||||
log_kv "Total scripts checked" "${total_scripts}"
|
|
||||||
log_kv "Missing copyright" "${missing_copyright}"
|
|
||||||
log_kv "Missing SPDX identifier" "${missing_spdx}"
|
|
||||||
log_kv "Missing FILE INFORMATION" "${missing_fileinfo}"
|
|
||||||
log_kv "Missing error handling" "${missing_error_handling}"
|
|
||||||
log_kv "Not executable" "${not_executable}"
|
|
||||||
log_separator
|
|
||||||
log_info "End time: $(log_timestamp)"
|
|
||||||
log_info "Duration: $(log_duration "$START_TIME" "$END_TIME")"
|
|
||||||
|
|
||||||
total_issues=$((missing_copyright + missing_spdx + missing_fileinfo + missing_error_handling + not_executable))
|
|
||||||
|
|
||||||
if [ "$total_issues" -eq 0 ]; then
|
|
||||||
log_success "SUCCESS: All scripts follow enterprise standards"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
log_error "FAILED: Found ${total_issues} standard violation(s)"
|
|
||||||
log_info "Run with -v flag for details on which scripts need updates"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.Test
|
|
||||||
# INGROUP: Repository.Validation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/run/smoke_test.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Basic smoke tests to verify repository structure and manifest validity
|
|
||||||
# NOTE: Quick validation checks for essential repository components
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Usage
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<-USAGE
|
|
||||||
Usage: $0 [OPTIONS]
|
|
||||||
|
|
||||||
Run basic smoke tests to verify repository structure and manifest validity.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$0 # Run all smoke tests
|
|
||||||
$0 --help # Show usage information
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Source common utilities
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# Check dependencies
|
|
||||||
check_dependencies python3
|
|
||||||
|
|
||||||
START_TIME=$(date +%s)
|
|
||||||
|
|
||||||
log_info "Running smoke tests for Moko-Cassiopeia repository"
|
|
||||||
log_info "Start time: $(log_timestamp)"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Test: Repository structure
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "Checking repository structure..."
|
|
||||||
|
|
||||||
assert_dir_exists "src"
|
|
||||||
assert_file_exists "README.md"
|
|
||||||
assert_file_exists "CHANGELOG.md"
|
|
||||||
assert_file_exists "LICENSE"
|
|
||||||
assert_file_exists "CONTRIBUTING.md"
|
|
||||||
|
|
||||||
log_info "✓ Repository structure valid"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Test: Manifest validation
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "Checking Joomla manifest..."
|
|
||||||
|
|
||||||
. "${SCRIPT_DIR}/lib/joomla_manifest.sh"
|
|
||||||
|
|
||||||
MANIFEST="$(find_manifest src)"
|
|
||||||
log_info "Found manifest: ${MANIFEST}"
|
|
||||||
|
|
||||||
VERSION="$(get_manifest_version "${MANIFEST}")"
|
|
||||||
NAME="$(get_manifest_name "${MANIFEST}")"
|
|
||||||
TYPE="$(get_manifest_type "${MANIFEST}")"
|
|
||||||
|
|
||||||
log_info "Extension: ${NAME} (${TYPE}) v${VERSION}"
|
|
||||||
|
|
||||||
# Verify manifest is well-formed XML
|
|
||||||
require_cmd python3
|
|
||||||
python3 - "${MANIFEST}" <<'PY'
|
|
||||||
import sys
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
manifest_path = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ET.parse(manifest_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
if root.tag != "extension":
|
|
||||||
print(f"ERROR: Root element must be <extension>, got <{root.tag}>")
|
|
||||||
sys.exit(1)
|
|
||||||
print("✓ Manifest XML is well-formed")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Failed to parse manifest: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
PY
|
|
||||||
|
|
||||||
log_info "✓ Manifest validation passed"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Test: Version alignment
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "Checking version alignment..."
|
|
||||||
|
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
|
||||||
log_warn "CHANGELOG.md not found, skipping version alignment check"
|
|
||||||
else
|
|
||||||
if grep -q "## \[${VERSION}\]" CHANGELOG.md; then
|
|
||||||
log_info "✓ Version ${VERSION} found in CHANGELOG.md"
|
|
||||||
else
|
|
||||||
log_warn "Version ${VERSION} not found in CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Test: Critical files syntax
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "Checking PHP syntax..."
|
|
||||||
|
|
||||||
if command -v php >/dev/null 2>&1; then
|
|
||||||
php_errors=0
|
|
||||||
failed_files=()
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
if ! php_output=$(php -l "$f" 2>&1); then
|
|
||||||
log_error "PHP syntax error in: $f"
|
|
||||||
echo " Error details:" >&2
|
|
||||||
echo "$php_output" | sed 's/^/ /' >&2
|
|
||||||
echo "" >&2
|
|
||||||
php_errors=$((php_errors + 1))
|
|
||||||
failed_files+=("$f")
|
|
||||||
fi
|
|
||||||
done < <(find src -type f -name '*.php' -print0 2>/dev/null)
|
|
||||||
|
|
||||||
if [ "${php_errors}" -eq 0 ]; then
|
|
||||||
log_info "✓ PHP syntax validation passed"
|
|
||||||
else
|
|
||||||
echo "Summary of PHP syntax errors:" >&2
|
|
||||||
echo " Total errors: ${php_errors}" >&2
|
|
||||||
echo " Failed files:" >&2
|
|
||||||
for f in "${failed_files[@]}"; do
|
|
||||||
echo " - $f" >&2
|
|
||||||
done
|
|
||||||
echo "" >&2
|
|
||||||
echo "To fix: Run 'php -l <filename>' on each failed file for detailed error messages." >&2
|
|
||||||
die "Found ${php_errors} PHP syntax errors"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_warn "PHP not available, skipping PHP syntax check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Summary
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "========================================="
|
|
||||||
log_info "Smoke tests completed successfully"
|
|
||||||
log_info "Extension: ${NAME}"
|
|
||||||
log_info "Version: ${VERSION}"
|
|
||||||
log_info "Type: ${TYPE}"
|
|
||||||
log_info "End time: $(log_timestamp)"
|
|
||||||
END_TIME=$(date +%s)
|
|
||||||
log_info "Duration: $(log_duration "$START_TIME" "$END_TIME")"
|
|
||||||
log_info "========================================="
|
|
||||||
181
scripts/run/validate_all.py
Executable file
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Run all validation scripts.
|
||||||
|
|
||||||
|
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
|
This file is part of a Moko Consulting project.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program (./LICENSE.md).
|
||||||
|
|
||||||
|
FILE INFORMATION
|
||||||
|
DEFGROUP: Script.Run
|
||||||
|
INGROUP: Validation.Runner
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/run/validate_all.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Run all validation scripts
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
Unused importImport of 'List' is not used. To fix the problem, remove the unused Concretely, in ## Unused import
Import of 'List' is not used.
---
To fix the problem, remove the unused <code>List</code> type from the <code>typing</code> import while keeping <code>Tuple</code>, which is used in the type annotation for <code>run_validation_script</code>. This keeps the code functionally identical but eliminates the unused import.</p>
<p>Concretely, in <code>scripts/run/validate_all.py</code> at line 36, change <code>from typing import List, Tuple</code> to <code>from typing import Tuple</code>. No other lines need to be modified, and no new methods, imports, or definitions are required.
Import of 'List' is not used. Import of 'List' is not used.
```suggestion
from typing import 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)
|
||||||
|
|
||||||
|
|
||||||
|
# Required validation scripts (must pass)
|
||||||
|
REQUIRED_SCRIPTS = [
|
||||||
|
"scripts/validate/manifest.py",
|
||||||
|
"scripts/validate/xml_wellformed.py",
|
||||||
|
"scripts/validate/workflows.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optional validation scripts (failures are warnings)
|
||||||
|
OPTIONAL_SCRIPTS = [
|
||||||
|
"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",
|
||||||
|
"scripts/validate/version_hierarchy.py",
|
||||||
|
]
|
||||||
|
The OPTIONAL_SCRIPTS list references Python files that don't exist in this PR. When these files are not found, they will be skipped, but the code still references them as if they should exist. Consider removing non-existent scripts from this list or adding a comment indicating they haven't been converted yet. The OPTIONAL_SCRIPTS list references Python files that don't exist in this PR. When these files are not found, they will be skipped, but the code still references them as if they should exist. Consider removing non-existent scripts from this list or adding a comment indicating they haven't been converted yet.
```suggestion
# NOTE:
# The following optional validators are planned but their Python implementations
# may not yet exist in this repository/PR:
# - 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
# - scripts/validate/version_hierarchy.py
# They are intentionally not included in OPTIONAL_SCRIPTS until implemented.
OPTIONAL_SCRIPTS = []
```
|
|||||||
|
|
||||||
|
|
||||||
|
def run_validation_script(script_path: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Run a validation script.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
script_path: Path to script
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, output)
|
||||||
|
"""
|
||||||
|
script = Path(script_path)
|
||||||
|
|
||||||
|
if not script.exists():
|
||||||
|
return (False, f"Script not found: {script_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", str(script)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
success = result.returncode == 0
|
||||||
|
|
||||||
|
return (success, output)
|
||||||
|
except Exception as e:
|
||||||
|
return (False, f"Error running script: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
common.log_section("Running All Validations")
|
||||||
|
print()
|
||||||
|
|
||||||
|
total_passed = 0
|
||||||
|
total_failed = 0
|
||||||
|
total_skipped = 0
|
||||||
|
|
||||||
|
# Run required scripts
|
||||||
|
common.log_info("=== Required Validations ===")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for script in REQUIRED_SCRIPTS:
|
||||||
|
script_name = Path(script).name
|
||||||
|
common.log_info(f"Running {script_name}...")
|
||||||
|
|
||||||
|
success, output = run_validation_script(script)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
common.log_success(f"✓ {script_name} passed")
|
||||||
|
total_passed += 1
|
||||||
|
else:
|
||||||
|
common.log_error(f"✗ {script_name} FAILED")
|
||||||
|
if output:
|
||||||
|
print(output)
|
||||||
|
total_failed += 1
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Run optional scripts
|
||||||
|
common.log_info("=== Optional Validations ===")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for script in OPTIONAL_SCRIPTS:
|
||||||
|
script_name = Path(script).name
|
||||||
|
|
||||||
|
if not Path(script).exists():
|
||||||
|
common.log_warn(f"⊘ {script_name} not found (skipped)")
|
||||||
|
total_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
common.log_info(f"Running {script_name}...")
|
||||||
|
|
||||||
|
success, output = run_validation_script(script)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
common.log_success(f"✓ {script_name} passed")
|
||||||
|
total_passed += 1
|
||||||
|
else:
|
||||||
|
common.log_warn(f"⚠ {script_name} failed (optional)")
|
||||||
|
if output:
|
||||||
|
print(output[:500]) # Limit output
|
||||||
|
total_failed += 1
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
common.log_section("Validation Summary")
|
||||||
|
common.log_kv("Total Passed", str(total_passed))
|
||||||
|
common.log_kv("Total Failed", str(total_failed))
|
||||||
|
common.log_kv("Total Skipped", str(total_skipped))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check if any required validations failed
|
||||||
|
required_failed = sum(
|
||||||
|
1 for script in REQUIRED_SCRIPTS
|
||||||
|
if Path(script).exists() and not run_validation_script(script)[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
if required_failed > 0:
|
||||||
|
common.log_error(f"{required_failed} required validation(s) failed")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
common.log_success("All required validations passed!")
|
||||||
|
|
||||||
|
if total_failed > 0:
|
||||||
|
common.log_warn(f"{total_failed} optional validation(s) failed")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.Run
|
|
||||||
# INGROUP: Repository.Validation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/run/validate_all.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Run all validation scripts and report results
|
|
||||||
# NOTE: Helpful for developers to run all checks before committing
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
. "${SCRIPT_DIR}/lib/logging.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Usage
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<-USAGE
|
|
||||||
Usage: $0 [OPTIONS]
|
|
||||||
|
|
||||||
Run all validation scripts and report results.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-v, --verbose Show detailed output from validation scripts
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
$0 # Run all validations in quiet mode
|
|
||||||
$0 -v # Run with verbose output
|
|
||||||
$0 --help # Show usage information
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
0 - All required checks passed
|
|
||||||
1 - One or more required checks failed
|
|
||||||
2 - Invalid arguments
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Configuration
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
VERBOSE="${1:-}"
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
case "${VERBOSE}" in
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
-v|--verbose)
|
|
||||||
VERBOSE="true"
|
|
||||||
;;
|
|
||||||
"")
|
|
||||||
VERBOSE="false"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "Invalid argument: ${VERBOSE}"
|
|
||||||
echo ""
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Check dependencies
|
|
||||||
check_dependencies python3
|
|
||||||
|
|
||||||
START_TIME=$(date +%s)
|
|
||||||
|
|
||||||
log_info "Start time: $(log_timestamp)"
|
|
||||||
|
|
||||||
REQUIRED_CHECKS=(
|
|
||||||
"manifest"
|
|
||||||
"xml_wellformed"
|
|
||||||
)
|
|
||||||
|
|
||||||
OPTIONAL_CHECKS=(
|
|
||||||
"changelog"
|
|
||||||
"language_structure"
|
|
||||||
"license_headers"
|
|
||||||
"no_secrets"
|
|
||||||
"paths"
|
|
||||||
"php_syntax"
|
|
||||||
"tabs"
|
|
||||||
"version_alignment"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Run validations
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_section "Repository Validation Suite"
|
|
||||||
log_info "Running all validation checks..."
|
|
||||||
log_separator
|
|
||||||
|
|
||||||
required_passed=0
|
|
||||||
required_failed=0
|
|
||||||
optional_passed=0
|
|
||||||
optional_failed=0
|
|
||||||
|
|
||||||
# Required checks
|
|
||||||
log_section "Required Checks"
|
|
||||||
for check in "${REQUIRED_CHECKS[@]}"; do
|
|
||||||
script="${SCRIPT_DIR}/validate/${check}.sh"
|
|
||||||
if [ ! -f "${script}" ]; then
|
|
||||||
log_error "Script not found: ${script}"
|
|
||||||
required_failed=$((required_failed + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_step "Running: ${check}"
|
|
||||||
# Always capture output for better error reporting
|
|
||||||
output=""
|
|
||||||
if output=$("${script}" 2>&1); then
|
|
||||||
log_success "✓ ${check}"
|
|
||||||
required_passed=$((required_passed + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && echo "$output"
|
|
||||||
else
|
|
||||||
log_error "✗ ${check} (FAILED)"
|
|
||||||
required_failed=$((required_failed + 1))
|
|
||||||
# Show error output for required checks regardless of verbose flag
|
|
||||||
echo "" >&2
|
|
||||||
echo "Error output:" >&2
|
|
||||||
echo "$output" >&2
|
|
||||||
echo "" >&2
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Optional checks
|
|
||||||
log_section "Optional Checks"
|
|
||||||
for check in "${OPTIONAL_CHECKS[@]}"; do
|
|
||||||
script="${SCRIPT_DIR}/validate/${check}.sh"
|
|
||||||
if [ ! -f "${script}" ]; then
|
|
||||||
log_warn "Script not found: ${script}"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_step "Running: ${check}"
|
|
||||||
# Capture output for better error reporting
|
|
||||||
output=""
|
|
||||||
if output=$("${script}" 2>&1); then
|
|
||||||
log_success "✓ ${check}"
|
|
||||||
optional_passed=$((optional_passed + 1))
|
|
||||||
[ "${VERBOSE}" = "true" ] && echo "$output"
|
|
||||||
else
|
|
||||||
log_warn "✗ ${check} (warnings/issues found)"
|
|
||||||
optional_failed=$((optional_failed + 1))
|
|
||||||
# Show brief error summary for optional checks, full output in verbose
|
|
||||||
if [ "${VERBOSE}" = "true" ]; then
|
|
||||||
echo "" >&2
|
|
||||||
echo "Error output:" >&2
|
|
||||||
echo "$output" >&2
|
|
||||||
echo "" >&2
|
|
||||||
else
|
|
||||||
# Show first few lines of error
|
|
||||||
echo "" >&2
|
|
||||||
echo "Error summary (run with -v for full details):" >&2
|
|
||||||
echo "$output" | head -5 >&2
|
|
||||||
echo "" >&2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Summary
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
log_separator
|
|
||||||
log_section "Validation Summary"
|
|
||||||
log_separator
|
|
||||||
|
|
||||||
log_kv "Required checks passed" "${required_passed}/${#REQUIRED_CHECKS[@]}"
|
|
||||||
log_kv "Required checks failed" "${required_failed}"
|
|
||||||
log_kv "Optional checks passed" "${optional_passed}/${#OPTIONAL_CHECKS[@]}"
|
|
||||||
log_kv "Optional checks with issues" "${optional_failed}"
|
|
||||||
|
|
||||||
log_separator
|
|
||||||
|
|
||||||
log_info "End time: $(log_timestamp)"
|
|
||||||
END_TIME=$(date +%s)
|
|
||||||
log_info "Duration: $(log_duration "$START_TIME" "$END_TIME")"
|
|
||||||
|
|
||||||
if [ "${required_failed}" -gt 0 ]; then
|
|
||||||
log_error "FAILED: ${required_failed} required check(s) failed"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
log_success "SUCCESS: All required checks passed"
|
|
||||||
if [ "${optional_failed}" -gt 0 ]; then
|
|
||||||
log_warn "Note: ${optional_failed} optional check(s) found issues"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Documentation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/changelog.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validates CHANGELOG.md structure and version entries
|
|
||||||
# NOTE: Ensures changelog compliance with Keep a Changelog standard
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 - <<'PY' "$1"
|
|
||||||
import json,sys
|
|
||||||
print(json.dumps(sys.argv[1]))
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
local msg="$1"
|
|
||||||
local extra="${2:-}"
|
|
||||||
if [ -n "${extra}" ]; then
|
|
||||||
printf '{"status":"fail","error":%s,%s}\n' "$(json_escape "${msg}")" "${extra}"
|
|
||||||
else
|
|
||||||
printf '{"status":"fail","error":%s}\n' "$(json_escape "${msg}")"
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ok() {
|
|
||||||
local extra="${1:-}"
|
|
||||||
if [ -n "${extra}" ]; then
|
|
||||||
printf '{"status":"ok",%s}\n' "${extra}"
|
|
||||||
else
|
|
||||||
printf '{"status":"ok"}\n'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Version resolution order:
|
|
||||||
# 1) explicit env: RELEASE_VERSION or VERSION
|
|
||||||
# 2) branch name (GITHUB_REF_NAME): rc/x.y.z or version/x.y.z or dev/x.y.z
|
|
||||||
# 3) tag name (GITHUB_REF_NAME): vX.Y.Z or vX.Y.Z-rc
|
|
||||||
# 4) git describe tag fallback
|
|
||||||
VERSION_IN="${RELEASE_VERSION:-${VERSION:-}}"
|
|
||||||
|
|
||||||
ref_name="${GITHUB_REF_NAME:-}"
|
|
||||||
|
|
||||||
infer_from_ref() {
|
|
||||||
local r="$1"
|
|
||||||
if printf '%s' "${r}" | grep -Eq '^(dev|rc|version)/[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
printf '%s' "${r#*/}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if printf '%s' "${r}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-rc)?$'; then
|
|
||||||
r="${r#v}"
|
|
||||||
r="${r%-rc}"
|
|
||||||
printf '%s' "${r}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
VERSION_RESOLVED=""
|
|
||||||
|
|
||||||
if [ -n "${VERSION_IN}" ]; then
|
|
||||||
if ! printf '%s' "${VERSION_IN}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
fail "Invalid version format in env" "\"version\":$(json_escape "${VERSION_IN}")"
|
|
||||||
fi
|
|
||||||
VERSION_RESOLVED="${VERSION_IN}"
|
|
||||||
else
|
|
||||||
if [ -n "${ref_name}" ]; then
|
|
||||||
if v="$(infer_from_ref "${ref_name}" 2>/dev/null)"; then
|
|
||||||
VERSION_RESOLVED="${v}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${VERSION_RESOLVED}" ]; then
|
|
||||||
tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
|
||||||
if [ -n "${tag}" ]; then
|
|
||||||
if v="$(infer_from_ref "${tag}" 2>/dev/null)"; then
|
|
||||||
VERSION_RESOLVED="${v}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${VERSION_RESOLVED}" ]; then
|
|
||||||
fail "Unable to infer version (set RELEASE_VERSION or VERSION, or use a versioned branch/tag)" "\"ref_name\":$(json_escape "${ref_name:-}" )"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
|
||||||
fail "CHANGELOG.md missing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -s "CHANGELOG.md" ]; then
|
|
||||||
fail "CHANGELOG.md is empty"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Core structural checks
|
|
||||||
# - Must contain at least one H2 heading with a bracketed version
|
|
||||||
# - Must contain a section for the resolved version
|
|
||||||
|
|
||||||
if ! grep -Eq '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md; then
|
|
||||||
fail "CHANGELOG.md has no version sections (expected headings like: ## [x.y.z])"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version section existence
|
|
||||||
if ! grep -Fq "## [${VERSION_RESOLVED}]" CHANGELOG.md; then
|
|
||||||
fail "CHANGELOG.md missing version section" "\"version\":$(json_escape "${VERSION_RESOLVED}")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional quality checks (warnings only)
|
|
||||||
warnings=()
|
|
||||||
|
|
||||||
# Expect a date on the same line as the version heading, like: ## [x.y.z] YYYY-MM-DD
|
|
||||||
if ! grep -Eq "^## \[${VERSION_RESOLVED}\] [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md; then
|
|
||||||
warnings+=("version_heading_date_missing_or_nonstandard")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Minimal section content: require at least one non-empty line between this version heading and the next heading.
|
|
||||||
python3 - <<'PY' "${VERSION_RESOLVED}" || true
|
|
||||||
import re,sys
|
|
||||||
ver = sys.argv[1]
|
|
||||||
text = open('CHANGELOG.md','r',encoding='utf-8').read().splitlines()
|
|
||||||
start = None
|
|
||||||
for i,line in enumerate(text):
|
|
||||||
if line.startswith(f"## [{ver}]"):
|
|
||||||
start = i
|
|
||||||
break
|
|
||||||
if start is None:
|
|
||||||
sys.exit(0)
|
|
||||||
end = len(text)
|
|
||||||
for j in range(start+1,len(text)):
|
|
||||||
if text[j].startswith('## ['):
|
|
||||||
end = j
|
|
||||||
break
|
|
||||||
block = [ln for ln in text[start+1:end] if ln.strip()]
|
|
||||||
# block contains at least one meaningful line (excluding blank)
|
|
||||||
if len(block) == 0:
|
|
||||||
print('WARN: version_section_empty')
|
|
||||||
PY
|
|
||||||
|
|
||||||
if grep -Fq 'WARN: version_section_empty' <(python3 - <<'PY' "${VERSION_RESOLVED}" 2>/dev/null || true
|
|
||||||
import sys
|
|
||||||
ver = sys.argv[1]
|
|
||||||
lines = open('CHANGELOG.md','r',encoding='utf-8').read().splitlines()
|
|
||||||
start = None
|
|
||||||
for i,l in enumerate(lines):
|
|
||||||
if l.startswith(f"## [{ver}]"):
|
|
||||||
start=i
|
|
||||||
break
|
|
||||||
if start is None:
|
|
||||||
sys.exit(0)
|
|
||||||
end=len(lines)
|
|
||||||
for j in range(start+1,len(lines)):
|
|
||||||
if lines[j].startswith('## ['):
|
|
||||||
end=j
|
|
||||||
break
|
|
||||||
block=[ln for ln in lines[start+1:end] if ln.strip()]
|
|
||||||
if len(block)==0:
|
|
||||||
print('WARN: version_section_empty')
|
|
||||||
PY
|
|
||||||
); then
|
|
||||||
warnings+=("version_section_empty")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Emit machine-readable report
|
|
||||||
if [ "${#warnings[@]}" -gt 0 ]; then
|
|
||||||
# Build JSON array safely
|
|
||||||
warn_json="["
|
|
||||||
sep=""
|
|
||||||
for w in "${warnings[@]}"; do
|
|
||||||
warn_json+="${sep}$(json_escape "${w}")"
|
|
||||||
sep=",";
|
|
||||||
done
|
|
||||||
warn_json+="]"
|
|
||||||
ok "\"version\":$(json_escape "${VERSION_RESOLVED}"),\"ref_name\":$(json_escape "${ref_name:-}"),\"warnings\":${warn_json}"
|
|
||||||
else
|
|
||||||
ok "\"version\":$(json_escape "${VERSION_RESOLVED}"),\"ref_name\":$(json_escape "${ref_name:-}"),\"warnings\":[]"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "changelog: ok (version=${VERSION_RESOLVED})"
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Joomla.Language
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/language_structure.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validates Joomla language directory structure and INI files
|
|
||||||
# NOTE: Ensures proper language file organization
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
LANG_ROOT="${LANG_ROOT:-${SRC_DIR}/language}"
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -d "${SRC_DIR}" ] || {
|
|
||||||
printf '{"status":"fail","error":%s}
|
|
||||||
' "$(json_escape "src directory missing")"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
python3 - <<'PY' "${LANG_ROOT}"
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
lang_root = Path(sys.argv[1])
|
|
||||||
|
|
||||||
# Language directory is optional for some extension types
|
|
||||||
if not lang_root.exists():
|
|
||||||
print(json.dumps({"status":"ok","lang_root":str(lang_root),"languages":[],"warnings":["language_root_missing"]}, ensure_ascii=False))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if not lang_root.is_dir():
|
|
||||||
print(json.dumps({"status":"fail","error":"language_root_not_directory","lang_root":str(lang_root)}, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
lang_dirs = sorted([p for p in lang_root.iterdir() if p.is_dir()])
|
|
||||||
|
|
||||||
# Joomla language tags: en-GB, fr-FR, etc.
|
|
||||||
pattern = re.compile(r'^[a-z]{2}-[A-Z]{2}$')
|
|
||||||
invalid = [p.name for p in lang_dirs if not pattern.match(p.name)]
|
|
||||||
|
|
||||||
warnings = []
|
|
||||||
|
|
||||||
# Soft expectation: en-GB exists if any language directories exist
|
|
||||||
if lang_dirs and not (lang_root / 'en-GB').exists():
|
|
||||||
warnings.append('en-GB_missing')
|
|
||||||
|
|
||||||
# Validate INI naming
|
|
||||||
missing_ini = []
|
|
||||||
nonmatching_ini = []
|
|
||||||
|
|
||||||
for d in lang_dirs:
|
|
||||||
ini_files = [p for p in d.glob('*.ini') if p.is_file()]
|
|
||||||
if not ini_files:
|
|
||||||
missing_ini.append(d.name)
|
|
||||||
continue
|
|
||||||
for ini in ini_files:
|
|
||||||
if not (ini.name.startswith(d.name + '.') or ini.name == f"{d.name}.ini"):
|
|
||||||
nonmatching_ini.append(str(ini))
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"status": "ok",
|
|
||||||
"lang_root": str(lang_root),
|
|
||||||
"languages": [d.name for d in lang_dirs],
|
|
||||||
"warnings": warnings,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Hard failures
|
|
||||||
if invalid:
|
|
||||||
result.update({"status":"fail","error":"invalid_language_tag_dir","invalid":invalid})
|
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if nonmatching_ini:
|
|
||||||
result.update({"status":"fail","error":"ini_name_mismatch","nonmatching_ini":nonmatching_ini[:50]})
|
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if missing_ini:
|
|
||||||
result.update({"status":"fail","error":"missing_ini_files","missing_ini":missing_ini})
|
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo "language_structure: ok"
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Licensing
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/license_headers.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Checks that source files contain SPDX license identifiers
|
|
||||||
# NOTE: Ensures licensing compliance across codebase
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -d "${SRC_DIR}" ] || {
|
|
||||||
printf '{"status":"fail","error":%s}\n' "$(json_escape "src directory missing")"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
python3 - <<'PY' "${SRC_DIR}"
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
src = Path(sys.argv[1])
|
|
||||||
|
|
||||||
exts = {'.php','.js','.css','.sh','.yml','.yaml','.xml'}
|
|
||||||
exclude_dirs = {'vendor','node_modules','dist','.git','build','tmp'}
|
|
||||||
|
|
||||||
missing = []
|
|
||||||
scanned = 0
|
|
||||||
|
|
||||||
for p in src.rglob('*'):
|
|
||||||
if not p.is_file():
|
|
||||||
continue
|
|
||||||
if any(part in exclude_dirs for part in p.parts):
|
|
||||||
continue
|
|
||||||
if p.suffix.lower() not in exts:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = p.read_bytes()[:2048]
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if b'\x00' in data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
scanned += 1
|
|
||||||
head = data.decode('utf-8', errors='replace')
|
|
||||||
if 'SPDX-License-Identifier:' not in head:
|
|
||||||
missing.append(str(p))
|
|
||||||
|
|
||||||
if missing:
|
|
||||||
print(json.dumps({
|
|
||||||
"status":"fail",
|
|
||||||
"error":"missing_spdx_identifier",
|
|
||||||
"scanned":scanned,
|
|
||||||
"missing_count":len(missing),
|
|
||||||
"missing":missing[:200]
|
|
||||||
}, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(json.dumps({"status":"ok","scanned":scanned,"missing_count":0}, ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo "license_headers
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Joomla.Manifest
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/manifest.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validates Joomla manifest XML structure and required fields
|
|
||||||
# NOTE: Ensures extension manifest compliance
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Input validation
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
|
|
||||||
log() { printf '%s\n' "$*"; }
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
log "ERROR: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate SRC_DIR
|
|
||||||
if [ ! -d "${SRC_DIR}" ]; then
|
|
||||||
fail "${SRC_DIR} directory missing. Set SRC_DIR environment variable or ensure 'src' directory exists."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate required dependencies
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
|
||||||
fail "python3 is required but not found. Please install Python 3."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Candidate discovery policy: prefer explicit known names, otherwise fall back to extension-root manifests.
|
|
||||||
# Goal: choose ONE manifest deterministically.
|
|
||||||
manifest_candidates=()
|
|
||||||
|
|
||||||
# Template
|
|
||||||
if [ -f "${SRC_DIR}/templateDetails.xml" ]; then
|
|
||||||
manifest_candidates+=("${SRC_DIR}/templateDetails.xml")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Package
|
|
||||||
while IFS= read -r f; do
|
|
||||||
[ -n "${f}" ] && manifest_candidates+=("${f}")
|
|
||||||
done < <(find "${SRC_DIR}" -maxdepth 4 -type f -name 'pkg_*.xml' 2>/dev/null | sort || true)
|
|
||||||
|
|
||||||
# Component
|
|
||||||
while IFS= read -r f; do
|
|
||||||
[ -n "${f}" ] && manifest_candidates+=("${f}")
|
|
||||||
done < <(find "${SRC_DIR}" -maxdepth 4 -type f -name 'com_*.xml' 2>/dev/null | sort || true)
|
|
||||||
|
|
||||||
# Module
|
|
||||||
while IFS= read -r f; do
|
|
||||||
[ -n "${f}" ] && manifest_candidates+=("${f}")
|
|
||||||
done < <(find "${SRC_DIR}" -maxdepth 4 -type f -name 'mod_*.xml' 2>/dev/null | sort || true)
|
|
||||||
|
|
||||||
# Plugin
|
|
||||||
while IFS= read -r f; do
|
|
||||||
[ -n "${f}" ] && manifest_candidates+=("${f}")
|
|
||||||
done < <(find "${SRC_DIR}" -maxdepth 6 -type f -name 'plg_*.xml' 2>/dev/null | sort || true)
|
|
||||||
|
|
||||||
# Fallback: any XML containing <extension ...>
|
|
||||||
if [ "${#manifest_candidates[@]}" -eq 0 ]; then
|
|
||||||
while IFS= read -r f; do
|
|
||||||
[ -n "${f}" ] && manifest_candidates+=("${f}")
|
|
||||||
done < <(grep -Rsl --include='*.xml' '<extension' "${SRC_DIR}" 2>/dev/null | sort || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#manifest_candidates[@]}" -eq 0 ]; then
|
|
||||||
{
|
|
||||||
echo "ERROR: No Joomla manifest XML found under ${SRC_DIR}" >&2
|
|
||||||
echo "" >&2
|
|
||||||
echo "Expected manifest file patterns:" >&2
|
|
||||||
echo " - Template: ${SRC_DIR}/templateDetails.xml" >&2
|
|
||||||
echo " - Package: ${SRC_DIR}/**/pkg_*.xml" >&2
|
|
||||||
echo " - Component: ${SRC_DIR}/**/com_*.xml" >&2
|
|
||||||
echo " - Module: ${SRC_DIR}/**/mod_*.xml" >&2
|
|
||||||
echo " - Plugin: ${SRC_DIR}/**/plg_*.xml" >&2
|
|
||||||
echo "" >&2
|
|
||||||
echo "Troubleshooting:" >&2
|
|
||||||
echo " 1. Verify the source directory exists: ls -la ${SRC_DIR}" >&2
|
|
||||||
echo " 2. Check for XML files: find ${SRC_DIR} -name '*.xml'" >&2
|
|
||||||
echo " 3. Ensure manifest contains <extension> root element" >&2
|
|
||||||
echo "" >&2
|
|
||||||
} >&2
|
|
||||||
fail "No manifest found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# De-duplicate while preserving order.
|
|
||||||
unique_candidates=()
|
|
||||||
for c in "${manifest_candidates[@]}"; do
|
|
||||||
seen=false
|
|
||||||
for u in "${unique_candidates[@]}"; do
|
|
||||||
if [ "${u}" = "${c}" ]; then
|
|
||||||
seen=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "${seen}" = "false" ]; then
|
|
||||||
unique_candidates+=("${c}")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
manifest_candidates=("${unique_candidates[@]}")
|
|
||||||
|
|
||||||
# Enforce single primary manifest.
|
|
||||||
if [ "${#manifest_candidates[@]}" -gt 1 ]; then
|
|
||||||
{
|
|
||||||
log "ERROR: Multiple manifest candidates detected. Resolve to exactly one primary manifest." >&2
|
|
||||||
log "" >&2
|
|
||||||
log "Found ${#manifest_candidates[@]} candidates:" >&2
|
|
||||||
for c in "${manifest_candidates[@]}"; do
|
|
||||||
log " - ${c}" >&2
|
|
||||||
done
|
|
||||||
log "" >&2
|
|
||||||
log "Resolution options:" >&2
|
|
||||||
log " 1. Remove redundant manifest files" >&2
|
|
||||||
log " 2. Move extra manifests outside ${SRC_DIR}" >&2
|
|
||||||
log " 3. Rename non-primary manifests to not match patterns (templateDetails.xml, pkg_*.xml, etc.)" >&2
|
|
||||||
log "" >&2
|
|
||||||
log "For package extensions, only the top-level package manifest should be in ${SRC_DIR}." >&2
|
|
||||||
log "Child extension manifests should be in subdirectories." >&2
|
|
||||||
log "" >&2
|
|
||||||
} >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
MANIFEST="${manifest_candidates[0]}"
|
|
||||||
|
|
||||||
if [ ! -s "${MANIFEST}" ]; then
|
|
||||||
fail "Manifest is empty: ${MANIFEST}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse with python for portability (xmllint not guaranteed).
|
|
||||||
python3 - <<'PY' "${MANIFEST}" || exit 1
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
manifest_path = Path(sys.argv[1])
|
|
||||||
|
|
||||||
def fail(msg, **ctx):
|
|
||||||
payload = {"status":"fail","error":msg, **ctx}
|
|
||||||
print(json.dumps(payload, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ET.parse(manifest_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
except Exception as e:
|
|
||||||
fail("XML parse failed", manifest=str(manifest_path), detail=str(e))
|
|
||||||
|
|
||||||
if root.tag != "extension":
|
|
||||||
fail("Root element must be <extension>", manifest=str(manifest_path), root=str(root.tag))
|
|
||||||
|
|
||||||
ext_type = (root.attrib.get("type") or "").strip().lower() or "unknown"
|
|
||||||
allowed_types = {"template","component","module","plugin","package","library","file","files"}
|
|
||||||
|
|
||||||
# Minimal required fields across most extension types.
|
|
||||||
name_el = root.find("name")
|
|
||||||
version_el = root.find("version")
|
|
||||||
|
|
||||||
name = (name_el.text or "").strip() if name_el is not None else ""
|
|
||||||
version = (version_el.text or "").strip() if version_el is not None else ""
|
|
||||||
|
|
||||||
missing = []
|
|
||||||
if not name:
|
|
||||||
missing.append("name")
|
|
||||||
if not version:
|
|
||||||
missing.append("version")
|
|
||||||
|
|
||||||
if ext_type not in allowed_types and ext_type != "unknown":
|
|
||||||
fail("Unsupported extension type", manifest=str(manifest_path), ext_type=ext_type)
|
|
||||||
|
|
||||||
# Type-specific expectations.
|
|
||||||
warnings = []
|
|
||||||
|
|
||||||
if ext_type == "plugin":
|
|
||||||
group = (root.attrib.get("group") or "").strip()
|
|
||||||
if not group:
|
|
||||||
missing.append("plugin.group")
|
|
||||||
|
|
||||||
files_el = root.find("files")
|
|
||||||
if files_el is None:
|
|
||||||
missing.append("files")
|
|
||||||
|
|
||||||
elif ext_type in {"component","module","template"}:
|
|
||||||
files_el = root.find("files")
|
|
||||||
if files_el is None:
|
|
||||||
missing.append("files")
|
|
||||||
|
|
||||||
elif ext_type == "package":
|
|
||||||
files_el = root.find("files")
|
|
||||||
if files_el is None:
|
|
||||||
missing.append("files")
|
|
||||||
else:
|
|
||||||
# Package should reference at least one child manifest.
|
|
||||||
file_nodes = files_el.findall("file")
|
|
||||||
if not file_nodes:
|
|
||||||
warnings.append("package.files has no <file> entries")
|
|
||||||
|
|
||||||
# Optional but commonly expected.
|
|
||||||
method = (root.attrib.get("method") or "").strip().lower()
|
|
||||||
if method and method not in {"upgrade","install"}:
|
|
||||||
warnings.append(f"unexpected extension method={method}")
|
|
||||||
|
|
||||||
# Provide a stable, machine-readable report.
|
|
||||||
if missing:
|
|
||||||
fail("Missing required fields", manifest=str(manifest_path), ext_type=ext_type, missing=missing, warnings=warnings)
|
|
||||||
|
|
||||||
print(json.dumps({
|
|
||||||
"status": "ok",
|
|
||||||
"manifest": str(manifest_path),
|
|
||||||
"ext_type": ext_type,
|
|
||||||
"name": name,
|
|
||||||
"version": version,
|
|
||||||
"warnings": warnings,
|
|
||||||
}, ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
|
|
||||||
# Human-friendly summary (kept short for CI logs).
|
|
||||||
log "manifest: ok (${MANIFEST})"
|
|
||||||
212
scripts/validate/no_secrets.py
Executable file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Scan for accidentally committed secrets and credentials.
|
||||||
|
|
||||||
|
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: Security
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/validate/no_secrets.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Scan for accidentally committed secrets and credentials
|
||||||
|
NOTE: High-signal pattern detection to prevent credential exposure
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, 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)
|
||||||
|
|
||||||
|
|
||||||
|
# High-signal patterns only. Any match is a hard fail.
|
||||||
|
SECRET_PATTERNS = [
|
||||||
|
# Private keys
|
||||||
|
r'-----BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----',
|
||||||
|
r'PuTTY-User-Key-File-',
|
||||||
|
# AWS keys
|
||||||
|
r'AKIA[0-9A-Z]{16}',
|
||||||
|
r'ASIA[0-9A-Z]{16}',
|
||||||
|
# GitHub tokens
|
||||||
|
r'ghp_[A-Za-z0-9]{36}',
|
||||||
|
r'gho_[A-Za-z0-9]{36}',
|
||||||
|
r'github_pat_[A-Za-z0-9_]{20,}',
|
||||||
|
# Slack tokens
|
||||||
|
r'xox[baprs]-[0-9A-Za-z-]{10,48}',
|
||||||
|
# Stripe keys
|
||||||
|
r'sk_live_[0-9a-zA-Z]{20,}',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Directories to exclude from scanning
|
||||||
|
EXCLUDE_DIRS = {
|
||||||
|
'vendor',
|
||||||
|
'node_modules',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'.git',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scan_file(filepath: Path, patterns: List[re.Pattern]) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Scan a file for secret patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to file to scan
|
||||||
|
patterns: Compiled regex patterns to search for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matches with file, line number, and content
|
||||||
|
"""
|
||||||
|
hits = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
for pattern in patterns:
|
||||||
|
if pattern.search(line):
|
||||||
|
hits.append({
|
||||||
|
'file': str(filepath),
|
||||||
|
'line': line_num,
|
||||||
|
'content': line.strip()[:100] # Limit to 100 chars
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
common.log_warn(f"Could not read {filepath}: {e}")
|
||||||
|
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def scan_directory(src_dir: str, patterns: List[re.Pattern]) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Recursively scan directory for secrets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_dir: Directory to scan
|
||||||
|
patterns: Compiled regex patterns
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all matches
|
||||||
|
"""
|
||||||
|
src_path = Path(src_dir)
|
||||||
|
all_hits = []
|
||||||
|
|
||||||
|
for item in src_path.rglob("*"):
|
||||||
|
# Skip directories
|
||||||
|
if not item.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip excluded directories
|
||||||
|
if any(excluded in item.parts for excluded in EXCLUDE_DIRS):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip binary files (heuristic)
|
||||||
|
try:
|
||||||
|
with open(item, 'rb') as f:
|
||||||
|
chunk = f.read(1024)
|
||||||
|
if b'\x00' in chunk: # Contains null bytes = likely binary
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Scan the file
|
||||||
|
hits = scan_file(item, patterns)
|
||||||
|
all_hits.extend(hits)
|
||||||
|
|
||||||
|
return all_hits
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Scan for accidentally committed secrets and credentials"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--src-dir",
|
||||||
|
default=os.environ.get("SRC_DIR", "src"),
|
||||||
|
help="Source directory to scan (default: src)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Check if source directory exists
|
||||||
|
if not Path(args.src_dir).is_dir():
|
||||||
|
result = {
|
||||||
|
"status": "fail",
|
||||||
|
"error": "src directory missing"
|
||||||
|
}
|
||||||
|
common.json_output(result)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Compile patterns
|
||||||
|
compiled_patterns = [re.compile(pattern) for pattern in SECRET_PATTERNS]
|
||||||
|
|
||||||
|
# Scan directory
|
||||||
|
hits = scan_directory(args.src_dir, compiled_patterns)
|
||||||
|
|
||||||
|
if hits:
|
||||||
|
# Limit to first 50 hits
|
||||||
|
hits = hits[:50]
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "fail",
|
||||||
|
"error": "secret_pattern_detected",
|
||||||
|
"hits": [{"hit": f"{h['file']}:{h['line']}: {h['content']}"} for h in hits]
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
# Also print human-readable output
|
||||||
|
print("\nERROR: Potential secrets detected!", file=sys.stderr)
|
||||||
|
print(f"\nFound {len(hits)} potential secret(s):", file=sys.stderr)
|
||||||
|
for hit in hits[:10]: # Show first 10 in detail
|
||||||
|
print(f" {hit['file']}:{hit['line']}", file=sys.stderr)
|
||||||
|
print(f" {hit['content']}", file=sys.stderr)
|
||||||
|
|
||||||
|
if len(hits) > 10:
|
||||||
|
print(f" ... and {len(hits) - 10} more", file=sys.stderr)
|
||||||
|
|
||||||
|
print("\nPlease remove any secrets and use environment variables or secret management instead.", file=sys.stderr)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"src_dir": args.src_dir
|
||||||
|
}
|
||||||
|
common.json_output(result)
|
||||||
|
print("no_secrets: ok")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Security
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/no_secrets.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Scan for accidentally committed secrets and credentials
|
|
||||||
# NOTE: High-signal pattern detection to prevent credential exposure
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -d "${SRC_DIR}" ] || {
|
|
||||||
printf '{"status":"fail","error":%s}
|
|
||||||
' "$(json_escape "src directory missing")"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# High-signal patterns only. Any match is a hard fail.
|
|
||||||
patterns=(
|
|
||||||
'-----BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----'
|
|
||||||
'PuTTY-User-Key-File-'
|
|
||||||
'AKIA[0-9A-Z]{16}'
|
|
||||||
'ASIA[0-9A-Z]{16}'
|
|
||||||
'ghp_[A-Za-z0-9]{36}'
|
|
||||||
'gho_[A-Za-z0-9]{36}'
|
|
||||||
'github_pat_[A-Za-z0-9_]{20,}'
|
|
||||||
'xox[baprs]-[0-9A-Za-z-]{10,48}'
|
|
||||||
'sk_live_[0-9a-zA-Z]{20,}'
|
|
||||||
)
|
|
||||||
|
|
||||||
regex="$(IFS='|'; echo "${patterns[*]}")"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
hits=$(grep -RInE --exclude-dir=vendor --exclude-dir=node_modules --exclude-dir=dist "${regex}" "${SRC_DIR}" 2>/dev/null)
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -n "${hits}" ]; then
|
|
||||||
{
|
|
||||||
echo '{"status":"fail","error":"secret_pattern_detected","hits":['
|
|
||||||
echo "${hits}" | head -n 50 | python3 - <<'PY'
|
|
||||||
import json,sys
|
|
||||||
lines=[l.rstrip('
|
|
||||||
') for l in sys.stdin.readlines() if l.strip()]
|
|
||||||
print("
|
|
||||||
".join([json.dumps({"hit":l})+"," for l in lines]).rstrip(','))
|
|
||||||
PY
|
|
||||||
echo ']}'
|
|
||||||
}
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '{"status":"ok","src_dir":%s}
|
|
||||||
' "$(json_escape "${SRC_DIR}")"
|
|
||||||
echo "no_secrets: ok"
|
|
||||||
169
scripts/validate/paths.py
Executable 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())
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Detect Windows-style path separators (backslashes)
|
|
||||||
# NOTE: Ensures cross-platform path compatibility
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Detect Windows-style path literals (backslashes) in repository files.
|
|
||||||
# Uses git ls-files -z and searches file contents for a literal backslash.
|
|
||||||
|
|
||||||
hits=()
|
|
||||||
hit_lines=()
|
|
||||||
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
# Skip common binary files by mime-type
|
|
||||||
if file --brief --mime-type "$f" | grep -qE '^(application|audio|image|video)/'; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Find lines with backslashes and collect details
|
|
||||||
if backslash_lines=$(grep -n -F $'\\' -- "$f" 2>/dev/null); then
|
|
||||||
hits+=("$f")
|
|
||||||
hit_lines+=("$backslash_lines")
|
|
||||||
fi
|
|
||||||
done < <(git ls-files -z)
|
|
||||||
|
|
||||||
if [ "${#hits[@]}" -gt 0 ]; then
|
|
||||||
echo "ERROR: Windows-style path literals detected" >&2
|
|
||||||
echo "" >&2
|
|
||||||
echo "Found backslashes in ${#hits[@]} file(s):" >&2
|
|
||||||
for i in "${!hits[@]}"; do
|
|
||||||
echo "" >&2
|
|
||||||
echo " File: ${hits[$i]}" >&2
|
|
||||||
echo " Lines with backslashes:" >&2
|
|
||||||
echo "${hit_lines[$i]}" | head -5 | sed 's/^/ /' >&2
|
|
||||||
if [ "$(echo "${hit_lines[$i]}" | wc -l)" -gt 5 ]; then
|
|
||||||
echo " ... and $(($(echo "${hit_lines[$i]}" | wc -l) - 5)) more" >&2
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "" >&2
|
|
||||||
echo "To fix:" >&2
|
|
||||||
echo " 1. Run: ./scripts/fix/paths.sh" >&2
|
|
||||||
echo " 2. Or manually replace backslashes (\\) with forward slashes (/)" >&2
|
|
||||||
echo " 3. Ensure paths use POSIX separators for cross-platform compatibility" >&2
|
|
||||||
echo "" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "paths: ok"
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Code.Quality
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/php_syntax.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validates PHP syntax using php -l on all PHP files
|
|
||||||
# NOTE: Requires PHP CLI to be available
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Validation timeout (seconds) - prevents hanging on problematic files
|
|
||||||
TIMEOUT="${VALIDATION_TIMEOUT:-30}"
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -d "${SRC_DIR}" ] || {
|
|
||||||
printf '{"status":"fail","error":%s}
|
|
||||||
' "$(json_escape "src directory missing")"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! command -v php >/dev/null 2>&1; then
|
|
||||||
printf '{"status":"ok","warning":"php_not_available","src_dir":%s}
|
|
||||||
' "$(json_escape "${SRC_DIR}")"
|
|
||||||
echo "php_syntax: ok (php not available)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
failed=0
|
|
||||||
checked=0
|
|
||||||
failed_files=()
|
|
||||||
failed_errors=()
|
|
||||||
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
checked=$((checked+1))
|
|
||||||
|
|
||||||
# Capture actual error output
|
|
||||||
error_output=""
|
|
||||||
|
|
||||||
# Use timeout if available to prevent hangs
|
|
||||||
if command -v timeout >/dev/null 2>&1; then
|
|
||||||
if ! error_output=$(timeout "${TIMEOUT}" php -l "$f" 2>&1); then
|
|
||||||
failed=1
|
|
||||||
failed_files+=("$f")
|
|
||||||
failed_errors+=("$error_output")
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if ! error_output=$(php -l "$f" 2>&1); then
|
|
||||||
failed=1
|
|
||||||
failed_files+=("$f")
|
|
||||||
failed_errors+=("$error_output")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done < <(find "${SRC_DIR}" -type f -name '*.php' -print0)
|
|
||||||
|
|
||||||
if [ "${failed}" -ne 0 ]; then
|
|
||||||
echo "ERROR: PHP syntax validation failed" >&2
|
|
||||||
echo "Files checked: ${checked}" >&2
|
|
||||||
echo "Files with errors: ${#failed_files[@]}" >&2
|
|
||||||
echo "" >&2
|
|
||||||
echo "Failed files and errors:" >&2
|
|
||||||
for i in "${!failed_files[@]}"; do
|
|
||||||
echo " File: ${failed_files[$i]}" >&2
|
|
||||||
echo " Error: ${failed_errors[$i]}" >&2
|
|
||||||
echo "" >&2
|
|
||||||
done
|
|
||||||
echo "" >&2
|
|
||||||
echo "To fix: Review and correct the syntax errors in the files listed above." >&2
|
|
||||||
echo "Run 'php -l <filename>' on individual files for detailed error messages." >&2
|
|
||||||
{
|
|
||||||
printf '{"status":"fail","error":"php_lint_failed","files_checked":%s,"failed_count":%s,"failed_files":[' "${checked}" "${#failed_files[@]}"
|
|
||||||
for i in "${!failed_files[@]}"; do
|
|
||||||
printf '%s' "$(json_escape "${failed_files[$i]}")"
|
|
||||||
[ "$i" -lt $((${#failed_files[@]} - 1)) ] && printf ','
|
|
||||||
done
|
|
||||||
printf ']}\n'
|
|
||||||
}
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '{"status":"ok","files_checked":%s}
|
|
||||||
' "${checked}"
|
|
||||||
echo "php_syntax: ok"
|
|
||||||
140
scripts/validate/tabs.py
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Detect TAB characters in YAML files where they are not allowed.
|
||||||
|
|
||||||
|
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: Code.Quality
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||||
|
PATH: /scripts/validate/tabs.py
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Detect TAB characters in YAML files where they are not allowed
|
||||||
|
NOTE: YAML specification forbids tab characters
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 get_yaml_files() -> List[str]:
|
||||||
|
"""
|
||||||
|
Get list of YAML files tracked by git.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of YAML file paths
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = common.run_command(
|
||||||
|
["git", "ls-files", "*.yml", "*.yaml"],
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
files = [f.strip() for f in result.stdout.split('\n') if f.strip()]
|
||||||
|
return files
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def check_tabs_in_file(filepath: str) -> List[Tuple[int, str]]:
|
||||||
|
"""
|
||||||
|
Check for tab characters in a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to file to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (line_number, line_content) tuples with tabs
|
||||||
|
"""
|
||||||
|
tabs_found = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
if '\t' in line:
|
||||||
|
tabs_found.append((line_num, line.rstrip()))
|
||||||
|
except Exception as e:
|
||||||
|
common.log_warn(f"Could not read {filepath}: {e}")
|
||||||
|
|
||||||
|
return tabs_found
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
yaml_files = get_yaml_files()
|
||||||
|
|
||||||
|
if not yaml_files:
|
||||||
|
print("No files to check")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
bad_files = []
|
||||||
|
all_violations = {}
|
||||||
|
|
||||||
|
for filepath in yaml_files:
|
||||||
|
tabs = check_tabs_in_file(filepath)
|
||||||
|
if tabs:
|
||||||
|
bad_files.append(filepath)
|
||||||
|
all_violations[filepath] = tabs
|
||||||
|
|
||||||
|
print(f"TAB found in {filepath}", file=sys.stderr)
|
||||||
|
print(" Lines with tabs:", file=sys.stderr)
|
||||||
|
|
||||||
|
# Show first 5 lines with tabs
|
||||||
|
for line_num, line_content in tabs[:5]:
|
||||||
|
print(f" {line_num}: {line_content[:80]}", file=sys.stderr)
|
||||||
|
|
||||||
|
if len(tabs) > 5:
|
||||||
|
print(f" ... and {len(tabs) - 5} more", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
|
||||||
|
if bad_files:
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
print("ERROR: Tabs found in repository files", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
print("YAML specification forbids tab characters.", file=sys.stderr)
|
||||||
|
print(f"Found tabs in {len(bad_files)} file(s):", file=sys.stderr)
|
||||||
|
for f in bad_files:
|
||||||
|
print(f" - {f}", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
print("To fix:", file=sys.stderr)
|
||||||
|
print(" 1. Run: python3 scripts/fix/tabs.py", file=sys.stderr)
|
||||||
|
The fix instructions reference the old shell script path. This should be updated to reference the Python script: The fix instructions reference the old shell script path. This should be updated to reference the Python script: `python3 scripts/fix/tabs.py`
|
|||||||
|
print(" 2. Or manually replace tabs with spaces in your editor", file=sys.stderr)
|
||||||
|
print(" 3. Configure your editor to use spaces (not tabs) for YAML files", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print("tabs: ok")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Code.Quality
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/tabs.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Detect TAB characters in YAML files where they are not allowed
|
|
||||||
# NOTE: YAML specification forbids tab characters
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Detect TAB characters in source files tracked by Git. Uses careful
|
|
||||||
# handling of filenames and avoids heredoc pitfalls.
|
|
||||||
|
|
||||||
# Check only YAML/YML files where tabs are not allowed by the YAML specification.
|
|
||||||
# Note: Other file types (PHP, JS, etc.) allow tabs per .editorconfig.
|
|
||||||
files=$(git ls-files '*.yml' '*.yaml' || true)
|
|
||||||
|
|
||||||
if [ -z "${files}" ]; then
|
|
||||||
echo "No files to check"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
bad=0
|
|
||||||
bad_files=()
|
|
||||||
bad_lines=()
|
|
||||||
|
|
||||||
while IFS= read -r f; do
|
|
||||||
# Find lines with tabs and store them
|
|
||||||
if tab_lines=$(grep -n $'\t' -- "$f" 2>/dev/null); then
|
|
||||||
echo "TAB found in $f" >&2
|
|
||||||
echo " Lines with tabs:" >&2
|
|
||||||
echo "$tab_lines" | head -5 | sed 's/^/ /' >&2
|
|
||||||
if [ "$(echo "$tab_lines" | wc -l)" -gt 5 ]; then
|
|
||||||
echo " ... and $(($(echo "$tab_lines" | wc -l) - 5)) more" >&2
|
|
||||||
fi
|
|
||||||
echo "" >&2
|
|
||||||
bad=1
|
|
||||||
bad_files+=("$f")
|
|
||||||
fi
|
|
||||||
done <<< "${files}"
|
|
||||||
|
|
||||||
if [ "${bad}" -ne 0 ]; then
|
|
||||||
echo "" >&2
|
|
||||||
echo "ERROR: Tabs found in repository files" >&2
|
|
||||||
echo "" >&2
|
|
||||||
echo "YAML specification forbids tab characters." >&2
|
|
||||||
echo "Found tabs in ${#bad_files[@]} file(s):" >&2
|
|
||||||
for f in "${bad_files[@]}"; do
|
|
||||||
echo " - $f" >&2
|
|
||||||
done
|
|
||||||
echo "" >&2
|
|
||||||
echo "To fix:" >&2
|
|
||||||
echo " 1. Run: ./scripts/fix/tabs.sh" >&2
|
|
||||||
echo " 2. Or manually replace tabs with spaces in your editor" >&2
|
|
||||||
echo " 3. Configure your editor to use spaces (not tabs) for YAML files" >&2
|
|
||||||
echo "" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "tabs: ok"
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Version.Management
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/version_alignment.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Checks that manifest version is documented in CHANGELOG.md
|
|
||||||
# NOTE: Ensures version consistency across repository
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Validate that the package/manifest version is present in CHANGELOG.md
|
|
||||||
# Uses a safe, quoted heredoc for the embedded Python to avoid shell
|
|
||||||
# interpolation and CRLF termination issues.
|
|
||||||
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: python3 not found" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 - <<'PY'
|
|
||||||
import sys, re, json, glob
|
|
||||||
|
|
||||||
# Locate a likely manifest under src
|
|
||||||
candidates = [
|
|
||||||
'src/templateDetails.xml',
|
|
||||||
'src/manifest.xml'
|
|
||||||
]
|
|
||||||
manifest = None
|
|
||||||
for p in candidates:
|
|
||||||
try:
|
|
||||||
with open(p, 'r', encoding='utf-8') as fh:
|
|
||||||
manifest = fh.read()
|
|
||||||
break
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if manifest is None:
|
|
||||||
# Fallback: search for an XML file under src that contains a version attribute
|
|
||||||
for fn in glob.glob('src/**/*.xml', recursive=True):
|
|
||||||
try:
|
|
||||||
with open(fn, 'r', encoding='utf-8') as fh:
|
|
||||||
txt = fh.read()
|
|
||||||
if 'version=' in txt:
|
|
||||||
manifest = txt
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if manifest is None:
|
|
||||||
print('WARNING: No manifest found, skipping version alignment check')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
m = re.search(r'version=["\']([0-9]+\.[0-9]+\.[0-9]+)["\']', manifest)
|
|
||||||
if not m:
|
|
||||||
print('ERROR: could not find semantic version in manifest')
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
manifest_version = m.group(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open('CHANGELOG.md', 'r', encoding='utf-8') as fh:
|
|
||||||
changelog = fh.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
print('ERROR: CHANGELOG.md not found')
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
if f'## [{manifest_version}]' not in changelog:
|
|
||||||
print(f'ERROR: version {manifest_version} missing from CHANGELOG.md')
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
print(json.dumps({'status': 'ok', 'version': manifest_version}))
|
|
||||||
sys.exit(0)
|
|
||||||
PY
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: Version.Management
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/version_hierarchy.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validate version hierarchy across branch prefixes
|
|
||||||
# NOTE: Checks for version conflicts across dev/, rc/, and version/ branches
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Functions
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
check_version_hierarchy() {
|
|
||||||
log_info "Checking version hierarchy across branch prefixes..."
|
|
||||||
|
|
||||||
local violations=0
|
|
||||||
|
|
||||||
# Get all branches with version-like names
|
|
||||||
local branches
|
|
||||||
branches=$(git ls-remote --heads origin 2>/dev/null | awk '{print $2}' | sed 's|refs/heads/||' || echo "")
|
|
||||||
|
|
||||||
if [ -z "$branches" ]; then
|
|
||||||
log_warn "No remote branches found or unable to fetch branches"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract versions from branches
|
|
||||||
local dev_versions=()
|
|
||||||
local rc_versions=()
|
|
||||||
local stable_versions=()
|
|
||||||
|
|
||||||
while IFS= read -r branch; do
|
|
||||||
if [[ "$branch" =~ ^dev/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then
|
|
||||||
dev_versions+=("${BASH_REMATCH[1]}")
|
|
||||||
elif [[ "$branch" =~ ^rc/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then
|
|
||||||
rc_versions+=("${BASH_REMATCH[1]}")
|
|
||||||
elif [[ "$branch" =~ ^version/([0-9]{2}\.[0-9]{2}\.[0-9]{2})$ ]]; then
|
|
||||||
stable_versions+=("${BASH_REMATCH[1]}")
|
|
||||||
fi
|
|
||||||
done <<< "$branches"
|
|
||||||
|
|
||||||
log_info "Found ${#dev_versions[@]} dev versions, ${#rc_versions[@]} RC versions, ${#stable_versions[@]} stable versions"
|
|
||||||
|
|
||||||
# Check for violations:
|
|
||||||
# 1. dev/ version that exists in rc/ or version/
|
|
||||||
for version in "${dev_versions[@]}"; do
|
|
||||||
# Check if exists in stable
|
|
||||||
for stable in "${stable_versions[@]}"; do
|
|
||||||
if [ "$version" = "$stable" ]; then
|
|
||||||
log_error "VIOLATION: Version $version exists in both dev/ and version/ branches"
|
|
||||||
violations=$((violations + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if exists in RC
|
|
||||||
for rc in "${rc_versions[@]}"; do
|
|
||||||
if [ "$version" = "$rc" ]; then
|
|
||||||
log_error "VIOLATION: Version $version exists in both dev/ and rc/ branches"
|
|
||||||
violations=$((violations + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
# 2. rc/ version that exists in version/
|
|
||||||
for version in "${rc_versions[@]}"; do
|
|
||||||
for stable in "${stable_versions[@]}"; do
|
|
||||||
if [ "$version" = "$stable" ]; then
|
|
||||||
log_error "VIOLATION: Version $version exists in both rc/ and version/ branches"
|
|
||||||
violations=$((violations + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $violations -eq 0 ]; then
|
|
||||||
log_info "✓ No version hierarchy violations found"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_error "✗ Found $violations version hierarchy violation(s)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "Version Hierarchy Validation"
|
|
||||||
log_info "============================="
|
|
||||||
log_info ""
|
|
||||||
|
|
||||||
check_version_hierarchy
|
|
||||||
exit_code=$?
|
|
||||||
|
|
||||||
log_info ""
|
|
||||||
log_info "============================="
|
|
||||||
if [ $exit_code -eq 0 ]; then
|
|
||||||
log_info "Version hierarchy validation passed"
|
|
||||||
else
|
|
||||||
log_error "Version hierarchy validation failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $exit_code
|
|
||||||
217
scripts/validate/workflows.py
Executable 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
|
||||||
Unused importImport of 'List' 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 ## 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.
Import of 'List' 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())
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validate GitHub Actions workflow files
|
|
||||||
# NOTE: Checks YAML syntax, structure, and best practices
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
. "${SCRIPT_DIR}/lib/common.sh"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Functions
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
validate_yaml_syntax() {
|
|
||||||
local file="$1"
|
|
||||||
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
|
||||||
log_warn "python3 not found, skipping YAML syntax validation"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 - "$file" <<'PYEOF'
|
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
|
||||||
import yaml
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
print("WARNING: PyYAML module not installed. Install with: pip3 install pyyaml")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
file_path = sys.argv[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(file_path, 'r') as f:
|
|
||||||
yaml.safe_load(f)
|
|
||||||
print(f"✓ Valid YAML: {file_path}")
|
|
||||||
sys.exit(0)
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
print(f"✗ YAML Error in {file_path}: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error reading {file_path}: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
PYEOF
|
|
||||||
}
|
|
||||||
|
|
||||||
check_no_tabs() {
|
|
||||||
local file="$1"
|
|
||||||
|
|
||||||
if grep -q $'\t' "$file"; then
|
|
||||||
log_error "✗ File contains tab characters: $file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
check_workflow_structure() {
|
|
||||||
local file="$1"
|
|
||||||
local filename=$(basename "$file")
|
|
||||||
|
|
||||||
# Check for required top-level keys
|
|
||||||
if ! grep -q "^name:" "$file"; then
|
|
||||||
log_warn "Missing 'name:' in $filename"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -q "^on:" "$file"; then
|
|
||||||
log_error "✗ Missing 'on:' trigger in $filename"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -q "^jobs:" "$file"; then
|
|
||||||
log_error "✗ Missing 'jobs:' in $filename"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_workflow_file() {
|
|
||||||
local file="$1"
|
|
||||||
local filename=$(basename "$file")
|
|
||||||
|
|
||||||
log_info "Validating: $filename"
|
|
||||||
|
|
||||||
local errors=0
|
|
||||||
|
|
||||||
# Check YAML syntax
|
|
||||||
if ! validate_yaml_syntax "$file"; then
|
|
||||||
errors=$((errors + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for tabs
|
|
||||||
if ! check_no_tabs "$file"; then
|
|
||||||
errors=$((errors + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check structure
|
|
||||||
if ! check_workflow_structure "$file"; then
|
|
||||||
errors=$((errors + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $errors -eq 0 ]; then
|
|
||||||
log_info "✓ $filename passed all checks"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_error "✗ $filename failed $errors check(s)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
log_info "GitHub Actions Workflow Validation"
|
|
||||||
log_info "==================================="
|
|
||||||
log_info ""
|
|
||||||
|
|
||||||
WORKFLOWS_DIR=".github/workflows"
|
|
||||||
|
|
||||||
if [ ! -d "$WORKFLOWS_DIR" ]; then
|
|
||||||
log_error "Workflows directory not found: $WORKFLOWS_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
total=0
|
|
||||||
passed=0
|
|
||||||
failed=0
|
|
||||||
|
|
||||||
for workflow in "$WORKFLOWS_DIR"/*.yml "$WORKFLOWS_DIR"/*.yaml; do
|
|
||||||
if [ ! -f "$workflow" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
total=$((total + 1))
|
|
||||||
|
|
||||||
if validate_workflow_file "$workflow"; then
|
|
||||||
passed=$((passed + 1))
|
|
||||||
else
|
|
||||||
failed=$((failed + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
done
|
|
||||||
|
|
||||||
log_info "==================================="
|
|
||||||
log_info "Summary:"
|
|
||||||
log_info " Total workflows: $total"
|
|
||||||
log_info " Passed: $passed"
|
|
||||||
log_info " Failed: $failed"
|
|
||||||
log_info "==================================="
|
|
||||||
|
|
||||||
if [ $failed -gt 0 ]; then
|
|
||||||
log_error "Workflow validation failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "All workflows validated successfully"
|
|
||||||
exit 0
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 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: XML.Validation
|
|
||||||
# REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
|
||||||
# PATH: /scripts/validate/xml_wellformed.sh
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Validates that all XML files are well-formed
|
|
||||||
# NOTE: Uses Python ElementTree for portable XML parsing
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SRC_DIR="${SRC_DIR:-src}"
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
local msg="$1"
|
|
||||||
local extra="${2:-}"
|
|
||||||
if [ -n "${extra}" ]; then
|
|
||||||
printf '{"status":"fail","error":%s,%s}\n' "$(json_escape "${msg}")" "${extra}"
|
|
||||||
else
|
|
||||||
printf '{"status":"fail","error":%s}\n' "$(json_escape "${msg}")"
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -d "${SRC_DIR}" ] || fail "src directory missing" "\"src_dir\":$(json_escape "${SRC_DIR}")"
|
|
||||||
|
|
||||||
python3 - <<'PY' "${SRC_DIR}"
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
src = Path(sys.argv[1])
|
|
||||||
xml_files = sorted([p for p in src.rglob('*.xml') if p.is_file()])
|
|
||||||
|
|
||||||
bad = []
|
|
||||||
for p in xml_files:
|
|
||||||
try:
|
|
||||||
ET.parse(p)
|
|
||||||
except Exception as e:
|
|
||||||
bad.append({"path": str(p), "error": str(e)})
|
|
||||||
|
|
||||||
if bad:
|
|
||||||
print(json.dumps({
|
|
||||||
"status": "fail",
|
|
||||||
"error": "XML parse failed",
|
|
||||||
"src_dir": str(src),
|
|
||||||
"xml_count": len(xml_files),
|
|
||||||
"bad_count": len(bad),
|
|
||||||
"bad": bad[:25],
|
|
||||||
}, ensure_ascii=False))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(json.dumps({
|
|
||||||
"status": "ok",
|
|
||||||
"src_dir": str(src),
|
|
||||||
"xml_count": len(xml_files),
|
|
||||||
}, ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo "xml_wellformed: ok"
|
|
||||||
The import of
extension_utilswill 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.