diff --git a/.github/scripts/utils/cache_manager.py b/.github/scripts/utils/cache_manager.py new file mode 100644 index 00000000..8bbcb19d --- /dev/null +++ b/.github/scripts/utils/cache_manager.py @@ -0,0 +1,42 @@ +import json +import hashlib +import os + +def get_hash(directory_path: str) -> str: + hasher = hashlib.sha256() + for root, dirs, files in os.walk(directory_path): + dirs.sort() + dirs[:] = [d for d in dirs if d not in {".git", ".cache", "__pycache__"}] + for file in sorted(files): + full_path = os.path.join(root, file) + rel_path = os.path.relpath(full_path, directory_path) + hasher.update(rel_path.encode("utf-8")) + with open(full_path, "rb") as f: + while chunk := f.read(8192): + hasher.update(chunk) + return hasher.hexdigest() + +class ValidationCache: + def __init__(self, cache_file: str = ".cache/plugin_validation.json"): + self.cache_file = cache_file + self.cache = self._load_cache() + + def _load_cache(self) -> dict: + if os.path.exists(self.cache_file): + try: + with open(self.cache_file, "r") as f: + return json.load(f) + except Exception: + return {} + return {} + + def is_cached(self, plugin_name: str, current_hash: str) -> bool: + return self.cache.get(plugin_name) == current_hash + + def update(self, plugin_name: str, current_hash: str) -> None: + self.cache[plugin_name] = current_hash + + def save(self) -> None: + os.makedirs(os.path.dirname(self.cache_file), exist_ok=True) + with open(self.cache_file, "w") as f: + json.dump(self.cache, f, indent=2) diff --git a/.github/scripts/validate.py b/.github/scripts/validate.py new file mode 100644 index 00000000..444c0836 --- /dev/null +++ b/.github/scripts/validate.py @@ -0,0 +1,58 @@ +import os +import sys +import json +import glob +from concurrent.futures import ThreadPoolExecutor +from utils.cache_manager import ValidationCache + +cache = ValidationCache() + +IGNORE_PATHS = {".git", ".github", ".cache", "node_modules", "scripts", "package-lock.json", "package.json"} + +def validate_plugin(plugin_path): + base_name = os.path.basename(plugin_path) + if base_name in IGNORE_PATHS or not os.path.exists(plugin_path): + return True, f"Skipped (system path): {base_name}" + + if cache.is_cached(plugin_path): + return True, f"Skipped (cached): {base_name}" + + errors = [] + if os.path.isdir(plugin_path): + json_files = glob.glob(os.path.join(plugin_path, "**/*.json"), recursive=True, include_hidden=True) + for jf in json_files: + try: + with open(jf, "r", encoding="utf-8") as f: + json.load(f) + except Exception as e: + errors.append(f"Invalid JSON format in {jf}: {str(e)}") + + if errors: + return False, "\n".join(errors) + + cache.update(plugin_path) + return True, f"Validated: {base_name}" + +def main(): + target_dir = os.getcwd() + entries = [os.path.join(target_dir, d) for d in os.listdir(target_dir) if d not in IGNORE_PATHS] + + print(f"🔍 Validating plugins across {len(entries)} target paths using parallel workers...") + + failed = False + with ThreadPoolExecutor(max_workers=8) as executor: + results = executor.map(validate_plugin, entries) + for success, msg in results: + print(f" -> {msg}") + if not success: + failed = True + + cache.save() + if failed: + print("❌ Plugin validation failed!") + sys.exit(1) + else: + print("✅ All plugin configurations validated successfully.") + +if __name__ == "__main__": + main() diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..f4b2bec8 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,30 @@ +name: Validate Plugins + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Restore Validation Cache + uses: actions/cache@v4 + with: + path: .cache/plugin_validation.json + key: plugin-cache-${{ runner.os }}-${{ hashFiles('.github/scripts/**') }}-${{ hashFiles('.github/scripts/**') }}-${{ github.sha }} + restore-keys: | + plugin-cache-${{ runner.os }}-${{ hashFiles('.github/scripts/**') }}- + + - name: Run Plugin Validator + run: python3 .github/scripts/validate.py diff --git a/.gitignore b/.gitignore index 43cc77f4..bfaa8f84 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ build/ # Logs *.log + +# Local validation cache +.cache/ diff --git a/README.md b/README.md index d600b7c6..d635dff3 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,10 @@ plugins/ ## License MIT + +## Local Validation + +Run parallel validator locally: +```bash +python3 .github/scripts/validate.py +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..07be5412 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "plugins", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}