#!/usr/bin/python3 # Version in sysadmin/ci-utilities should be single source of truth # SPDX-FileCopyrightText: 2023 Alexander Lohnau # SPDX-License-Identifier: BSD-2-Clause import argparse import os import subprocess import json from typing import Dict, List import yaml parser = argparse.ArgumentParser(description='Check JSON schema for files in repository') parser.add_argument('--schemafile', required=True, help='Path to the schema file') parser.add_argument('--check-all', default=False, action='store_true', help='If all files should be checked or only the changed files') parser.add_argument('--verbose', default=False, action='store_true') args = parser.parse_args() def get_changed_files() -> List[str]: result = subprocess.run(['git', 'diff', '--cached', '--name-only'], capture_output=True, text=True) return [file for file in result.stdout.splitlines() if file.endswith('.json')] def get_all_files() -> List[str]: files = [] for root, _, filenames in os.walk('.'): for filename in filenames: if filename.endswith('.json'): files.append(os.path.join(root, filename)) return files def filter_excluded_included_json_files(files: List[str]) -> List[str]: config_file = '.kde-ci.yml' # Check if the file exists if os.path.exists(config_file): with open(config_file, 'r') as file: config = yaml.safe_load(file) else: if args.verbose: print(f'{config_file} does not exist in current directory') config = {} # Extract excluded files, used for tests that intentionally have broken files excluded_files = ['compile_commands.json', 'ci-utilities', 'sysadmin-repo-metadata'] if 'Options' in config and 'json-validate-ignore' in config['Options']: excluded_files += config['Options']['json-validate-ignore'] # Find JSON files filtered_files = [] for file_path in files: if not any(excluded_file in file_path for excluded_file in excluded_files): filtered_files.append(file_path) # "include" overrides the "ignore" files, so if a file is both included and excluded, it will be included if 'Options' in config and 'json-validate-include' in config['Options']: filtered_files += config['Options']['json-validate-include'] return filtered_files if args.check_all: files = get_all_files() else: files = get_changed_files() files = filter_excluded_included_json_files(files) if not files: exit(0) fallback_schema = args.schemafile if not fallback_schema.startswith(('http://', 'https://')): fallback_schema = os.path.abspath(fallback_schema) files_schema_dict: Dict[str, list[str]] = {} has_error = False for jsonfilepath in files: with open(jsonfilepath) as f: try: jsoncontent = json.load(f) except json.decoder.JSONDecodeError as e: print(f"Unable to load {jsonfilepath}: {e}") has_error = True continue schema_to_use: str = jsoncontent['$schema'] if '$schema' in jsoncontent else fallback_schema # In case the wildcard catches a schema definition, there is no point to validate it if schema_to_use.startswith("http://json-schema.org/") or schema_to_use.startswith("https://json-schema.org/"): print(f"skipping validation of {jsonfilepath} due to it being json schema") continue if not schema_to_use.startswith(('http://', 'https://')): schema_to_use = os.path.abspath(os.path.join(os.path.dirname(jsonfilepath), schema_to_use)) if not schema_to_use in files_schema_dict: files_schema_dict[schema_to_use] = [jsonfilepath] files_schema_dict[schema_to_use].append(jsonfilepath) for schema, files in files_schema_dict.items(): if args.verbose: files_option = ' '.join(files) print(f"Validating {files_option} with {schema}") result = subprocess.run(['check-jsonschema', *files, '--schemafile', schema]) # Don't exit right away in case we have multiple schemas to validate if result.returncode != 0: has_error = True if has_error: exit(1)