mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +00:00
refactor: centralize validation logic with validate_with helper (#412)
* chore: sonarcloud fixes * chore: coderabbit cr fixes
This commit is contained in:
@@ -27,57 +27,45 @@ class CustomValidator(BaseValidator):
|
||||
self.boolean_validator = BooleanValidator()
|
||||
self.file_validator = FileValidator()
|
||||
|
||||
def validate_inputs(self, inputs: dict[str, str]) -> bool: # pylint: disable=too-many-branches
|
||||
def validate_inputs(self, inputs: dict[str, str]) -> bool:
|
||||
"""Validate validate-inputs action inputs."""
|
||||
valid = True
|
||||
|
||||
# Validate action/action-type input
|
||||
if "action" in inputs or "action-type" in inputs:
|
||||
action_input = inputs.get("action") or inputs.get("action-type", "")
|
||||
# Check for empty action
|
||||
action_key = self.get_key_variant(inputs, "action", "action-type")
|
||||
if action_key:
|
||||
action_input = inputs[action_key]
|
||||
if action_input == "":
|
||||
self.add_error("Action name cannot be empty")
|
||||
valid = False
|
||||
# Allow GitHub expressions
|
||||
elif action_input.startswith("${{") and action_input.endswith("}}"):
|
||||
pass # GitHub expressions are valid
|
||||
# Check for dangerous characters
|
||||
elif any(
|
||||
char in action_input
|
||||
for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r", "/"]
|
||||
):
|
||||
self.add_error(f"Invalid characters in action name: {action_input}")
|
||||
valid = False
|
||||
# Validate action name format (should be lowercase with hyphens or underscores)
|
||||
elif action_input and not re.match(r"^[a-z][a-z0-9_-]*[a-z0-9]$", action_input):
|
||||
self.add_error(f"Invalid action name format: {action_input}")
|
||||
valid = False
|
||||
elif not self.is_github_expression(action_input):
|
||||
# Only validate non-GitHub expressions
|
||||
if any(
|
||||
char in action_input
|
||||
for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r", "/"]
|
||||
):
|
||||
self.add_error(f"Invalid characters in action name: {action_input}")
|
||||
valid = False
|
||||
elif action_input and not re.match(r"^[a-z][a-z0-9_-]*[a-z0-9]$", action_input):
|
||||
self.add_error(f"Invalid action name format: {action_input}")
|
||||
valid = False
|
||||
|
||||
# Validate rules-file if provided
|
||||
if inputs.get("rules-file"):
|
||||
result = self.file_validator.validate_file_path(inputs["rules-file"], "rules-file")
|
||||
for error in self.file_validator.errors:
|
||||
if error not in self.errors:
|
||||
self.add_error(error)
|
||||
self.file_validator.clear_errors()
|
||||
if not result:
|
||||
valid = False
|
||||
valid &= self.validate_with(
|
||||
self.file_validator, "validate_file_path", inputs["rules-file"], "rules-file"
|
||||
)
|
||||
|
||||
# Validate fail-on-error boolean
|
||||
if "fail-on-error" in inputs:
|
||||
value = inputs["fail-on-error"]
|
||||
# Reject empty string
|
||||
if value == "":
|
||||
self.add_error("fail-on-error cannot be empty")
|
||||
valid = False
|
||||
elif value:
|
||||
result = self.boolean_validator.validate_boolean(value, "fail-on-error")
|
||||
for error in self.boolean_validator.errors:
|
||||
if error not in self.errors:
|
||||
self.add_error(error)
|
||||
self.boolean_validator.clear_errors()
|
||||
if not result:
|
||||
valid = False
|
||||
valid &= self.validate_with(
|
||||
self.boolean_validator, "validate_boolean", value, "fail-on-error"
|
||||
)
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
@@ -895,7 +895,7 @@ optional_inputs:
|
||||
self.validator._validate_multi_value_enum("test", "input", valid_values=["only_one"])
|
||||
raise AssertionError("Should raise ValueError for single value")
|
||||
except ValueError as e:
|
||||
assert "at least 2 valid values" in str(e)
|
||||
assert ">= 2 values" in str(e)
|
||||
|
||||
# Should raise ValueError if more than max_values
|
||||
try:
|
||||
@@ -906,7 +906,7 @@ optional_inputs:
|
||||
)
|
||||
raise AssertionError("Should raise ValueError for 11 values")
|
||||
except ValueError as e:
|
||||
assert "at most 10 valid values" in str(e)
|
||||
assert "<= 10 values" in str(e)
|
||||
|
||||
def test_validate_exit_code_list_valid(self):
|
||||
"""Test exit code list validation with valid values."""
|
||||
|
||||
@@ -227,3 +227,82 @@ class BaseValidator(ABC):
|
||||
or ("${{" in value and "}}" in value)
|
||||
or (value.strip().startswith("${{") and value.strip().endswith("}}"))
|
||||
)
|
||||
|
||||
def propagate_errors(self, validator: BaseValidator, result: bool) -> bool:
|
||||
"""Copy errors from another validator and return result.
|
||||
|
||||
Args:
|
||||
validator: The validator to copy errors from
|
||||
result: The validation result to return
|
||||
|
||||
Returns:
|
||||
The result parameter unchanged
|
||||
"""
|
||||
for error in validator.errors:
|
||||
if error not in self.errors:
|
||||
self.add_error(error)
|
||||
validator.clear_errors()
|
||||
return result
|
||||
|
||||
def validate_with(
|
||||
self, validator: BaseValidator, method: str, *args: Any, **kwargs: Any
|
||||
) -> bool:
|
||||
"""Call validator method and propagate errors.
|
||||
|
||||
Args:
|
||||
validator: The validator instance to use
|
||||
method: The method name to call on the validator
|
||||
*args: Positional arguments to pass to the method
|
||||
**kwargs: Keyword arguments to pass to the method
|
||||
|
||||
Returns:
|
||||
The validation result
|
||||
"""
|
||||
result = getattr(validator, method)(*args, **kwargs)
|
||||
return self.propagate_errors(validator, result)
|
||||
|
||||
def validate_enum(
|
||||
self,
|
||||
value: str,
|
||||
name: str,
|
||||
valid_values: list[str],
|
||||
*,
|
||||
case_sensitive: bool = False,
|
||||
) -> bool:
|
||||
"""Validate value is one of allowed options.
|
||||
|
||||
Args:
|
||||
value: The value to validate
|
||||
name: The name of the input for error messages
|
||||
valid_values: List of allowed values
|
||||
case_sensitive: Whether comparison should be case sensitive
|
||||
|
||||
Returns:
|
||||
True if value is valid or empty/GitHub expression, False otherwise
|
||||
"""
|
||||
if not value or self.is_github_expression(value):
|
||||
return True
|
||||
check = value if case_sensitive else value.lower()
|
||||
allowed = valid_values if case_sensitive else [v.lower() for v in valid_values]
|
||||
if check not in allowed:
|
||||
self.add_error(f"Invalid {name}: {value}. Must be one of: {', '.join(valid_values)}")
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_key_variant(inputs: dict[str, str], *variants: str) -> str | None:
|
||||
"""Get first matching key variant from inputs.
|
||||
|
||||
Useful for inputs that may use underscore or hyphen variants.
|
||||
|
||||
Args:
|
||||
inputs: Dictionary of inputs to check
|
||||
*variants: Key variants to search for in order
|
||||
|
||||
Returns:
|
||||
The first matching key, or None if no match
|
||||
"""
|
||||
for key in variants:
|
||||
if key in inputs:
|
||||
return key
|
||||
return None
|
||||
|
||||
@@ -5,6 +5,7 @@ This validator automatically applies validation based on input naming convention
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -424,7 +425,10 @@ class ConventionBasedValidator(BaseValidator):
|
||||
if error not in self.errors:
|
||||
self.add_error(error)
|
||||
# Clear the module's errors after copying
|
||||
validator_module.errors = []
|
||||
if hasattr(validator_module, "clear_errors"):
|
||||
validator_module.clear_errors()
|
||||
else:
|
||||
validator_module.errors = []
|
||||
|
||||
return result
|
||||
# Method not found, skip validation
|
||||
@@ -629,7 +633,8 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Args:
|
||||
value: The comma-separated list value
|
||||
input_name: The input name for error messages
|
||||
item_pattern: Regex pattern each item must match (default: alphanumeric+hyphens+underscores)
|
||||
item_pattern: Regex pattern each item must match
|
||||
(default: alphanumeric+hyphens+underscores)
|
||||
valid_items: Optional list of valid items for enum-style validation
|
||||
check_injection: Whether to check for shell injection patterns
|
||||
item_name: Descriptive name for items in error messages (e.g., "linter", "extension")
|
||||
@@ -654,8 +659,6 @@ class ConventionBasedValidator(BaseValidator):
|
||||
... )
|
||||
True
|
||||
"""
|
||||
import re
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
|
||||
@@ -895,14 +898,12 @@ class ConventionBasedValidator(BaseValidator):
|
||||
|
||||
# Validate valid_values count
|
||||
if len(valid_values) < min_values:
|
||||
raise ValueError(
|
||||
f"Multi-value enum requires at least {min_values} valid values, got {len(valid_values)}"
|
||||
)
|
||||
msg = f"Multi-value enum needs >= {min_values} values, got {len(valid_values)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
if len(valid_values) > max_values:
|
||||
raise ValueError(
|
||||
f"Multi-value enum supports at most {max_values} valid values, got {len(valid_values)}"
|
||||
)
|
||||
msg = f"Multi-value enum allows <= {max_values} values, got {len(valid_values)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
@@ -1024,8 +1025,6 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
import re
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
|
||||
@@ -1123,8 +1122,6 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Valid: "0", "0,1,2", "5,10,15", "0,130", ""
|
||||
Invalid: "256", "0,256", "-1", "0,abc", "0,,1"
|
||||
"""
|
||||
import re
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
|
||||
@@ -1169,8 +1166,10 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Args:
|
||||
value: The key-value list value (comma-separated KEY=VALUE pairs)
|
||||
input_name: The input name for error messages
|
||||
key_pattern: Regex pattern for key validation (default: alphanumeric+underscores+hyphens)
|
||||
check_injection: Whether to check for shell injection patterns in values (default: True)
|
||||
key_pattern: Regex pattern for key validation
|
||||
(default: alphanumeric+underscores+hyphens)
|
||||
check_injection: Whether to check for shell injection patterns
|
||||
in values (default: True)
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
@@ -1179,7 +1178,6 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Valid: "KEY=value", "KEY1=value1,KEY2=value2", "BUILD_ARG=hello", ""
|
||||
Invalid: "KEY", "=value", "KEY=", "KEY=value,", "KEY=val;whoami"
|
||||
"""
|
||||
import re
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
@@ -1260,8 +1258,6 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
import re
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
|
||||
@@ -1412,8 +1408,6 @@ class ConventionBasedValidator(BaseValidator):
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
import re
|
||||
|
||||
if not value or value.strip() == "":
|
||||
return True # Optional
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ class TokenValidator(BaseValidator):
|
||||
"""Validator for various authentication tokens."""
|
||||
|
||||
# Token patterns for different token types (based on official GitHub documentation)
|
||||
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#githubs-token-formats
|
||||
# See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/
|
||||
# about-authentication-to-github#githubs-token-formats
|
||||
# Note: The lengths include the prefix
|
||||
TOKEN_PATTERNS: ClassVar[dict[str, str]] = {
|
||||
# Personal access token (classic):
|
||||
|
||||
Reference in New Issue
Block a user