Files
actions/validate-inputs/scripts/debug-validator.py
Ismo Vuorinen 7061aafd35 chore: add tests, update docs and actions (#299)
* docs: update documentation

* feat: validate-inputs has it's own pyproject

* security: mask DOCKERHUB_PASSWORD

* chore: add tokens, checkout, recrete docs, integration tests

* fix: add `statuses: write` permission to pr-lint
2025-10-18 13:09:19 +03:00

390 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""Debug utility for testing validators.
This tool helps debug validation issues by:
- Testing validators directly with sample inputs
- Showing which validator would be used for inputs
- Tracing validation flow
- Reporting detailed error information
"""
from __future__ import annotations
import argparse
import json
import logging
import sys
from pathlib import Path
from typing import TYPE_CHECKING
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from validators.conventions import ConventionBasedValidator
if TYPE_CHECKING:
from validators.base import BaseValidator
from validators.registry import ValidatorRegistry
# Set up logging
logging.basicConfig(
level=logging.INFO,
format="%(levelname)-8s %(name)s: %(message)s",
)
logger = logging.getLogger("debug-validator")
class ValidatorDebugger:
"""Debugging utility for validators."""
def __init__(self, *, verbose: bool = False) -> None:
"""Initialize the debugger.
Args:
verbose: Enable verbose output
"""
self.verbose = verbose
self.registry = ValidatorRegistry()
if verbose:
logging.getLogger().setLevel(logging.DEBUG)
def debug_action(self, action_type: str, inputs: dict[str, str]) -> None:
"""Debug validation for an action.
Args:
action_type: The action type to validate
inputs: Dictionary of inputs to validate
"""
print(f"\n{'=' * 60}")
print(f"Debugging: {action_type}")
print(f"{'=' * 60}\n")
# Get the validator
print("1. Getting validator...")
validator = self.registry.get_validator(action_type)
print(f" Validator: {validator.__class__.__name__}")
print(f" Module: {validator.__class__.__module__}\n")
# Show required inputs
if hasattr(validator, "get_required_inputs"):
required = validator.get_required_inputs()
if required:
print("2. Required inputs:")
for inp in required:
status = "" if inp in inputs else ""
print(f" {status} {inp}")
print()
# Validate inputs
print("3. Validating inputs...")
result = validator.validate_inputs(inputs)
print(f" Result: {'PASS' if result else 'FAIL'}\n")
# Show errors
if validator.errors:
print("4. Validation errors:")
for i, error in enumerate(validator.errors, 1):
print(f" {i}. {error}")
print()
else:
print("4. No validation errors\n")
# Show validation details for each input
if self.verbose:
self.show_input_details(validator, inputs)
def show_input_details(self, validator: BaseValidator, inputs: dict[str, str]) -> None:
"""Show detailed validation info for each input.
Args:
validator: The validator instance
inputs: Dictionary of inputs
"""
print("5. Input validation details:")
# If it's a convention-based validator, show which validator would be used
if isinstance(validator, ConventionBasedValidator):
for input_name, value in inputs.items():
mapper = getattr(validator, "_convention_mapper", None)
validator_type = mapper.get_validator_type(input_name) if mapper else None
print(f"\n {input_name}:")
print(f" Value: {value[:50]}..." if len(value) > 50 else f" Value: {value}")
print(f" Validator: {validator_type or 'BaseValidator (default)'}")
# Try to validate individually to see specific errors
if validator_type:
# Use registry to get validator instance
sub_validator = self.registry.get_validator_by_type(validator_type)
if sub_validator:
# Clear previous errors
sub_validator.clear_errors()
# Validate based on type
valid = self._validate_single_input(
sub_validator,
validator_type,
input_name,
value,
)
print(f" Valid: {'' if valid else ''}")
if sub_validator.errors:
for error in sub_validator.errors:
print(f" Error: {error}")
print()
def _validate_single_input(
self,
validator: BaseValidator,
validator_type: str,
input_name: str,
value: str,
) -> bool:
"""Validate a single input with appropriate method.
Args:
validator: The validator instance
validator_type: Type of validator
input_name: Name of the input
value: Value to validate
Returns:
True if valid, False otherwise
"""
# Map validator types to validation methods
method_map = {
"boolean": "validate_boolean",
"version": "validate_flexible_version",
"token": "validate_github_token",
"numeric": "validate_numeric_range",
"file": "validate_file_path",
"network": "validate_url",
"docker": "validate_image_name",
"security": "validate_no_injection",
"codeql": "validate_languages",
}
method_name = method_map.get(validator_type)
if method_name and hasattr(validator, method_name):
method = getattr(validator, method_name)
# Handle methods with different signatures
if validator_type == "numeric":
# Numeric validator needs min/max values
# Try to detect from input name
if "retries" in input_name:
return method(value, 1, 10, input_name)
if "limit" in input_name or "max" in input_name:
return method(value, 0, 100, input_name)
return method(value, 0, 999999, input_name)
if validator_type == "codeql":
# CodeQL expects a list
return method([value], input_name)
# Most validators take (value, field_name)
return method(value, input_name)
# Fallback to validate_inputs
return validator.validate_inputs({input_name: value})
def test_input_matching(self, input_names: list[str]) -> None:
"""Test which validators would be used for input names.
Args:
input_names: List of input names to test
"""
print(f"\n{'=' * 60}")
print("Input Name Matching Test")
print(f"{'=' * 60}\n")
conv_validator = ConventionBasedValidator("test")
mapper = getattr(conv_validator, "_convention_mapper", None)
if not mapper:
print("Convention mapper not available")
return
print(f"{'Input Name':<30} {'Validator':<20} {'Pattern Type'}")
print("-" * 70)
for name in input_names:
validator_type = mapper.get_validator_type(name)
# Determine pattern type
pattern_type = "none"
if validator_type:
if name in mapper.PATTERNS.get("exact", {}):
pattern_type = "exact"
elif any(name.startswith(p) for p in mapper.PATTERNS.get("prefix", {})):
pattern_type = "prefix"
elif any(name.endswith(p) for p in mapper.PATTERNS.get("suffix", {})):
pattern_type = "suffix"
elif any(p in name for p in mapper.PATTERNS.get("contains", {})):
pattern_type = "contains"
validator_display = validator_type or "BaseValidator"
print(f"{name:<30} {validator_display:<20} {pattern_type}")
def validate_file(self, filepath: Path) -> None:
"""Validate inputs from a JSON file.
Args:
filepath: Path to JSON file with inputs
"""
try:
with filepath.open() as f:
data = json.load(f)
action_type = data.get("action_type", "unknown")
inputs = data.get("inputs", {})
self.debug_action(action_type, inputs)
except json.JSONDecodeError:
logger.exception("Invalid JSON in %s", filepath)
sys.exit(1)
except FileNotFoundError:
logger.exception("File not found: %s", filepath)
sys.exit(1)
def list_validators(self) -> None:
"""List all available validators."""
print(f"\n{'=' * 60}")
print("Available Validators")
print(f"{'=' * 60}\n")
# Core validators
from validators.boolean import BooleanValidator
from validators.codeql import CodeQLValidator
from validators.docker import DockerValidator
from validators.file import FileValidator
from validators.network import NetworkValidator
from validators.numeric import NumericValidator
from validators.security import SecurityValidator
from validators.token import TokenValidator
from validators.version import VersionValidator
validators = [
("BooleanValidator", BooleanValidator, "Boolean values (true/false)"),
("VersionValidator", VersionValidator, "Version strings (SemVer/CalVer)"),
("TokenValidator", TokenValidator, "Authentication tokens"),
("NumericValidator", NumericValidator, "Numeric ranges"),
("FileValidator", FileValidator, "File paths"),
("NetworkValidator", NetworkValidator, "URLs, emails, hostnames"),
("DockerValidator", DockerValidator, "Docker images, tags, platforms"),
("SecurityValidator", SecurityValidator, "Security patterns, injection"),
("CodeQLValidator", CodeQLValidator, "CodeQL languages and queries"),
]
print("Core Validators:")
for name, _cls, desc in validators:
print(f" {name:<20} - {desc}")
# Check for custom validators
print("\nCustom Validators:")
custom_found = False
for action_dir in Path().iterdir():
if action_dir.is_dir() and not action_dir.name.startswith((".", "_")):
custom_file = action_dir / "CustomValidator.py"
if custom_file.exists():
print(f" {action_dir.name:<20} - Custom validator")
custom_found = True
if not custom_found:
print(" None found")
print()
def main() -> None:
"""Main entry point for the debug utility."""
parser = argparse.ArgumentParser(
description="Debug validator for GitHub Actions inputs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Test specific inputs
%(prog)s --action docker-build --input "image-name=myapp" --input "tag=v1.0.0"
# Test from JSON file
%(prog)s --file test-inputs.json
# Test input name matching
%(prog)s --test-matching github-token node-version dry-run
# List available validators
%(prog)s --list-validators
""",
)
parser.add_argument(
"--action",
"-a",
help="Action type to debug",
)
parser.add_argument(
"--input",
"-i",
action="append",
help="Input in format name=value (can be repeated)",
)
parser.add_argument(
"--file",
"-f",
type=Path,
help="JSON file with action_type and inputs",
)
parser.add_argument(
"--test-matching",
"-t",
nargs="+",
help="Test which validators match input names",
)
parser.add_argument(
"--list-validators",
"-l",
action="store_true",
help="List all available validators",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose output",
)
args = parser.parse_args()
# Create debugger
debugger = ValidatorDebugger(verbose=args.verbose)
# Handle different modes
if args.list_validators:
debugger.list_validators()
elif args.test_matching:
debugger.test_input_matching(args.test_matching)
elif args.file:
debugger.validate_file(args.file)
elif args.action and args.input:
# Parse inputs
inputs = {}
for input_str in args.input:
if "=" not in input_str:
logger.error("Invalid input format: %s (expected name=value)", input_str)
sys.exit(1)
name, value = input_str.split("=", 1)
inputs[name] = value
debugger.debug_action(args.action, inputs)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()