mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
* 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
390 lines
13 KiB
Python
Executable File
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()
|