mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
* feat: first pass simplification
* refactor: simplify actions repository structure
Major simplification reducing actions from 44 to 30:
Consolidations:
- Merge biome-check + biome-fix → biome-lint (mode: check/fix)
- Merge eslint-check + eslint-fix → eslint-lint (mode: check/fix)
- Merge prettier-check + prettier-fix → prettier-lint (mode: check/fix)
- Merge 5 version-detect actions → language-version-detect (language param)
Removals:
- common-file-check, common-retry (better served by external tools)
- docker-publish-gh, docker-publish-hub (consolidated into docker-publish)
- github-release (redundant with existing tooling)
- set-git-config (no longer needed)
- version-validator (functionality moved to language-version-detect)
Fixes:
- Rewrite docker-publish to use official Docker actions directly
- Update validate-inputs example (eslint-fix → eslint-lint)
- Update tests and documentation for new structure
Result: ~6,000 lines removed, cleaner action catalog, maintained functionality.
* refactor: complete action simplification and cleanup
Remove deprecated actions and update remaining actions:
Removed:
- common-file-check, common-retry: utility actions
- docker-publish-gh, docker-publish-hub: replaced by docker-publish wrapper
- github-release, version-validator, set-git-config: no longer needed
- Various version-detect actions: replaced by language-version-detect
Updated:
- docker-publish: rewrite as simple wrapper using official Docker actions
- validate-inputs: update example (eslint-fix → eslint-lint)
- Multiple actions: update configurations and remove deprecated dependencies
- Tests: update integration/unit tests for new structure
- Documentation: update README, remove test for deleted actions
Configuration updates:
- Linter configs, ignore files for new structure
- Makefile, pyproject.toml updates
* fix: enforce POSIX compliance in GitHub workflows
Convert all workflow shell scripts to POSIX-compliant sh:
Critical fixes:
- Replace bash with sh in all shell declarations
- Replace [[ with [ for test conditions
- Replace == with = for string comparisons
- Replace set -euo pipefail with set -eu
- Split compound AND conditions into separate [ ] tests
Files updated:
- .github/workflows/test-actions.yml (7 shell declarations, 10 test operators)
- .github/workflows/security-suite.yml (set -eu)
- .github/workflows/action-security.yml (2 shell declarations)
- .github/workflows/pr-lint.yml (3 shell declarations)
- .github/workflows/issue-stats.yml (1 shell declaration)
Ensures compatibility with minimal sh implementations and aligns with
CLAUDE.md standards requiring POSIX shell compliance across all scripts.
All tests pass: 764 pytest tests, 100% coverage.
* fix: add missing permissions for private repository support
Add critical permissions to pr-lint workflow for private repositories:
Workflow-level permissions:
+ packages: read - Access private npm/PyPI/Composer packages
Job-level permissions:
+ packages: read - Access private packages during dependency installation
+ checks: write - Create and update check runs
Fixes failures when:
- Installing private npm packages from GitHub Packages
- Installing private Composer dependencies
- Installing private Python packages
- Creating status checks with github-script
Valid permission scopes per actionlint:
actions, attestations, checks, contents, deployments, discussions,
id-token, issues, models, packages, pages, pull-requests,
repository-projects, security-events, statuses
Note: "workflows" and "metadata" are NOT valid permission scopes
(they are PAT-only scopes or auto-granted respectively).
* docs: update readmes
* fix: replace bash-specific 'source' with POSIX '.' command
Replace all occurrences of 'source' with '.' (dot) for POSIX compliance:
Changes in python-lint-fix/action.yml:
- Line 165: source .venv/bin/activate → . .venv/bin/activate
- Line 179: source .venv/bin/activate → . .venv/bin/activate
- Line 211: source .venv/bin/activate → . .venv/bin/activate
Also fixed bash-specific test operator:
- Line 192: [[ "$FAIL_ON_ERROR" == "true" ]] → [ "$FAIL_ON_ERROR" = "true" ]
The 'source' command is bash-specific. POSIX sh uses '.' (dot) to source files.
Both commands have identical functionality but '.' is portable across all
POSIX-compliant shells.
* security: fix code injection vulnerability in docker-publish
Fix CodeQL code injection warning (CWE-094, CWE-095, CWE-116):
Issue: inputs.context was used directly in GitHub Actions expression
without sanitization at line 194, allowing potential code injection
by external users.
Fix: Use environment variable indirection to prevent expression injection:
- Added env.BUILD_CONTEXT to capture inputs.context
- Changed context parameter to use ${{ env.BUILD_CONTEXT }}
Environment variables are evaluated after expression compilation,
preventing malicious code execution during workflow parsing.
Security Impact: Medium severity (CVSS 5.0)
Identified by: GitHub Advanced Security (CodeQL)
Reference: https://github.com/ivuorinen/actions/pull/353#pullrequestreview-3481935924
* security: prevent credential persistence in pr-lint checkout
Add persist-credentials: false to checkout step to mitigate untrusted
checkout vulnerability. This prevents GITHUB_TOKEN from being accessible
to potentially malicious PR code.
Fixes: CodeQL finding CWE-829 (untrusted checkout on privileged workflow)
* fix: prevent security bot from overwriting unrelated comments
Replace broad string matching with unique HTML comment marker for
identifying bot-generated comments. Previously, any comment containing
'Security Analysis' or '🔐 GitHub Actions Permissions' would be
overwritten, causing data loss.
Changes:
- Add unique marker: <!-- security-analysis-bot-comment -->
- Prepend marker to generated comment body
- Update comment identification to use marker only
- Add defensive null check for comment.body
This fixes critical data loss bug where user comments could be
permanently overwritten by the security analysis bot.
Follows same proven pattern as test-actions.yml coverage comments.
* improve: show concise permissions diff instead of full blocks
Replace verbose full-block permissions diff with line-by-line changes.
Now shows only added/removed permissions, making output much more
readable.
Changes:
- Parse permissions into individual lines
- Compare old vs new to identify actual changes
- Show only removed (-) and added (+) lines in diff
- Collapse unchanged permissions into details section (≤3 items)
- Show count summary for many unchanged permissions (>3 items)
Example output:
Before: 30+ lines showing entire permissions block
After: 3-5 lines showing only what changed
This addresses user feedback that permissions changes were too verbose.
* security: add input validation and trust model documentation
Add comprehensive security validation for docker-publish action to prevent
code injection attacks (CWE-094, CWE-116).
Changes:
- Add validation for context input (reject absolute paths, warn on URLs)
- Add validation for dockerfile input (reject absolute/URL paths)
- Document security trust model in README
- Add best practices for secure usage
- Explain validation rules and threat model
Prevents malicious actors from:
- Building from arbitrary file system locations
- Fetching Dockerfiles from untrusted remote sources
- Executing code injection through build context manipulation
Addresses: CodeRabbit review comments #2541434325, #2541549615
Fixes: GitHub Advanced Security code injection findings
* security: replace unmaintained nick-fields/retry with step-security/retry
Replace nick-fields/retry with step-security/retry across all 4 actions:
- csharp-build/action.yml
- php-composer/action.yml
- go-build/action.yml
- ansible-lint-fix/action.yml
The nick-fields/retry action has security vulnerabilities and low maintenance.
step-security/retry is a drop-in replacement with full API compatibility.
All inputs (timeout_minutes, max_attempts, command, retry_wait_seconds) are
compatible. Using SHA-pinned version for security.
Addresses CodeRabbit review comment #2541549598
* test: add is_input_required() helper function
Add helper function to check if an action input is required, reducing
duplication across test suites.
The function:
- Takes action_file and input_name as parameters
- Uses validation_core.py to query the 'required' property
- Returns 0 (success) if input is required
- Returns 1 (failure) if input is optional
This DRY improvement addresses CodeRabbit review comment #2541549572
* feat: add mode validation convention mapping
Add "mode" to the validation conventions mapping for lint actions
(eslint-lint, biome-lint, prettier-lint).
Note: The update-validators script doesn't currently recognize "string"
as a validator type, so mode validation coverage remains at 93%. The
actions already have inline validation for mode (check|fix), so this is
primarily for improving coverage metrics.
Addresses part of CodeRabbit review comment #2541549570
(validation coverage improvement)
* docs: fix CLAUDE.md action counts and add missing action
- Update action count from 31 to 29 (line 42)
- Add missing 'action-versioning' to Utilities category (line 74)
Addresses CodeRabbit review comments #2541553130 and #2541553110
* docs: add security considerations to docker-publish
Add security documentation to both action.yml header and README.md:
- Trust model explanation
- Input validation details for context and dockerfile
- Attack prevention information
- Best practices for secure usage
The documentation was previously removed when README was autogenerated.
Now documented in both places to ensure it persists.
* fix: correct step ID reference in docker-build
Fix incorrect step ID reference in platforms output:
- Changed steps.platforms.outputs.built to steps.detect-platforms.outputs.platforms
- The step is actually named 'detect-platforms' not 'platforms'
- Ensures output correctly references the detect-platforms step defined at line 188
* fix: ensure docker-build platforms output is always available
Make detect-platforms step unconditional to fix broken output contract.
The platforms output (line 123) references steps.detect-platforms.outputs.platforms,
but the step only ran when auto-detect-platforms was true (default: false).
This caused undefined output in most cases.
Changes:
- Remove 'if' condition from detect-platforms step
- Step now always runs and always produces platforms output
- When auto-detect is false: outputs configured architectures
- When auto-detect is true: outputs detected platforms or falls back to architectures
- Add '|| true' to grep to prevent errors when no platforms detected
Fixes CodeRabbit review comment #2541824904
* security: remove env var indirection in docker-publish BUILD_CONTEXT
Remove BUILD_CONTEXT env var indirection to address GitHub Advanced Security alert.
The inputs.context is validated at lines 137-147 (rejects absolute paths, warns on URLs)
before being used, so the env var indirection is unnecessary and triggers false positive
code injection warnings.
Changes:
- Remove BUILD_CONTEXT env var (line 254)
- Use inputs.context directly (line 256 → 254)
- Input validation remains in place (lines 137-147)
Fixes GitHub Advanced Security code injection alerts (comments #2541405269, #2541522320)
* feat: implement mode_enum validator for lint actions
Add mode_enum validator to validate mode inputs in linting actions.
Changes to conventions.py:
- Add 'mode_enum' to exact_matches mapping (line 215)
- Add 'mode_enum' to PHP-specific validators list (line 560)
- Implement _validate_mode_enum() method (lines 642-660)
- Validates mode values against ['check', 'fix']
- Returns clear error messages for invalid values
Updated rules.yml files:
- biome-lint: Add mode: mode_enum convention
- eslint-lint: Add mode: mode_enum convention
- prettier-lint: Add mode: mode_enum convention
- All rules.yml: Fix YAML formatting with yamlfmt
This addresses PR #353 comment #2541522326 which reported that mode validation
was being skipped due to unrecognized 'string' type, reducing coverage to 93%.
Tested with biome-lint action - correctly rejects invalid values and accepts
valid 'check' and 'fix' values.
* docs: update action count from 29 to 30 in CLAUDE.md
Update two references to action count in CLAUDE.md:
- Line 42: repository_overview memory description
- Line 74: Repository Structure section header
The repository has 30 actions total (29 listed + validate-inputs).
Addresses PR #353 comment #2541549588.
* docs: use pinned version ref in language-version-detect README
Change usage example from @main to @v2025 for security best practices.
Using pinned version refs (instead of @main) ensures:
- Predictable behavior across workflow runs
- Protection against breaking changes
- Better security through immutable references
Follows repository convention documented in main README and CLAUDE.md.
Addresses PR #353 comment #2541549588.
* refactor: remove deprecated add-snippets input from codeql-analysis
Remove add-snippets input which has been deprecated by GitHub's CodeQL action
and no longer has any effect.
Changes:
- Remove add-snippets input definition (lines 93-96)
- Remove reference in init step (line 129)
- Remove reference in analyze step (line 211)
- Regenerate README and rules.yml
This is a non-breaking change since:
- Default was 'false' (minimal usage expected)
- GitHub's action already ignores this parameter
- Aligns with recent repository simplification efforts
* feat: add mode_enum validator and update rules
Add mode_enum validator support for lint actions and regenerate all validation rules:
Validator Changes:
- Add mode_enum to action_overrides for biome-lint, eslint-lint, prettier-lint
- Remove deprecated add-snippets from codeql-analysis overrides
Rules Updates:
- All 29 action rules.yml files regenerated with consistent YAML formatting
- biome-lint, eslint-lint, prettier-lint now validate mode input (check/fix)
- Improved coverage for lint actions (79% → 83% for biome, 93% for eslint, 79% for prettier)
Documentation:
- Fix language-version-detect README to use @v2025 (not @main)
- Remove outdated docker-publish security docs (now handled by official actions)
This completes PR #353 review feedback implementation.
* fix: replace bash-specific $'\n' with POSIX-compliant printf
Replace non-POSIX $'\n' syntax in tag building loop with printf-based
approach that works in any POSIX shell.
Changed:
- Line 216: tags="${tags}"$'\n'"${image}:${tag}"
+ Line 216: tags="$(printf '%s\n%s' "$tags" "${image}:${tag}")"
This ensures docker-publish/action.yml runs correctly on systems using
/bin/sh instead of bash.
661 lines
23 KiB
Python
661 lines
23 KiB
Python
"""Convention-based validator that uses naming patterns to determine validation rules.
|
|
|
|
This validator automatically applies validation based on input naming conventions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml # pylint: disable=import-error
|
|
|
|
from .base import BaseValidator
|
|
from .convention_mapper import ConventionMapper
|
|
|
|
TOKEN_TYPES = {
|
|
"github": "github_token",
|
|
"npm": "npm_token",
|
|
"docker": "docker_token",
|
|
}
|
|
|
|
VERSION_MAPPINGS = {
|
|
"python": "python_version",
|
|
"node": "node_version",
|
|
"go": "go_version",
|
|
"php": "php_version",
|
|
"terraform": "terraform_version",
|
|
"dotnet": "dotnet_version",
|
|
"net": "dotnet_version",
|
|
}
|
|
|
|
FILE_TYPES = {
|
|
"yaml": "yaml_file",
|
|
"yml": "yaml_file",
|
|
"json": "json_file",
|
|
}
|
|
|
|
|
|
class ConventionBasedValidator(BaseValidator):
|
|
"""Validator that applies validation based on naming conventions.
|
|
|
|
Automatically detects validation requirements based on input names
|
|
and applies appropriate validators.
|
|
"""
|
|
|
|
def __init__(self, action_type: str) -> None:
|
|
"""Initialize the convention-based validator.
|
|
|
|
Args:
|
|
action_type: The type of GitHub Action being validated
|
|
"""
|
|
super().__init__(action_type)
|
|
self._rules = self.load_rules()
|
|
self._validator_modules: dict[str, Any] = {}
|
|
self._convention_mapper = ConventionMapper() # Use the ConventionMapper
|
|
self._load_validator_modules()
|
|
|
|
def _load_validator_modules(self) -> None:
|
|
"""Lazy-load validator modules as needed."""
|
|
# These will be imported as needed to avoid circular imports
|
|
|
|
def load_rules(self, rules_path: Path | None = None) -> dict[str, Any]:
|
|
"""Load validation rules from YAML file.
|
|
|
|
Args:
|
|
rules_path: Optional path to the rules YAML file
|
|
|
|
Returns:
|
|
Dictionary of validation rules
|
|
"""
|
|
if rules_path and rules_path.exists():
|
|
rules_file = rules_path
|
|
else:
|
|
# Find the rules file for this action in the action folder
|
|
# Convert underscores back to dashes for the folder name
|
|
action_name = self.action_type.replace("_", "-")
|
|
project_root = Path(__file__).parent.parent.parent
|
|
rules_file = project_root / action_name / "rules.yml"
|
|
|
|
if not rules_file.exists():
|
|
# Return default empty rules if no rules file exists
|
|
return {
|
|
"action_type": self.action_type,
|
|
"required_inputs": [],
|
|
"optional_inputs": [],
|
|
"conventions": {},
|
|
"overrides": {},
|
|
}
|
|
|
|
try:
|
|
with Path(rules_file).open() as f:
|
|
rules = yaml.safe_load(f) or {}
|
|
|
|
# Ensure all expected keys exist
|
|
rules.setdefault("required_inputs", [])
|
|
rules.setdefault("optional_inputs", [])
|
|
rules.setdefault("conventions", {})
|
|
rules.setdefault("overrides", {})
|
|
|
|
# Build conventions from optional_inputs if not explicitly set
|
|
if not rules["conventions"] and rules["optional_inputs"]:
|
|
conventions = {}
|
|
optional_inputs = rules["optional_inputs"]
|
|
|
|
# Handle both list and dict formats for optional_inputs
|
|
if isinstance(optional_inputs, list):
|
|
# List format: just input names
|
|
for input_name in optional_inputs:
|
|
conventions[input_name] = self._infer_validator_type(input_name, {})
|
|
elif isinstance(optional_inputs, dict):
|
|
# Dict format: input names with config
|
|
for input_name, input_config in optional_inputs.items():
|
|
conventions[input_name] = self._infer_validator_type(
|
|
input_name, input_config
|
|
)
|
|
|
|
rules["conventions"] = conventions
|
|
|
|
return rules
|
|
except Exception:
|
|
return {
|
|
"action_type": self.action_type,
|
|
"required_inputs": [],
|
|
"optional_inputs": [],
|
|
"conventions": {},
|
|
"overrides": {},
|
|
}
|
|
|
|
def _infer_validator_type(self, input_name: str, input_config: dict[str, Any]) -> str | None:
|
|
"""Infer the validator type from input name and configuration.
|
|
|
|
Args:
|
|
input_name: The name of the input
|
|
input_config: The input configuration from rules
|
|
|
|
Returns:
|
|
The inferred validator type or None
|
|
"""
|
|
# Check for explicit validator type in config
|
|
if isinstance(input_config, dict) and "validator" in input_config:
|
|
return input_config["validator"]
|
|
|
|
# Infer based on name patterns
|
|
name_lower = input_name.lower().replace("-", "_")
|
|
|
|
# Try to determine validator type
|
|
validator_type = self._check_exact_matches(name_lower)
|
|
|
|
if validator_type is None:
|
|
validator_type = self._check_pattern_based_matches(name_lower)
|
|
|
|
return validator_type
|
|
|
|
def _check_exact_matches(self, name_lower: str) -> str | None:
|
|
"""Check for exact pattern matches."""
|
|
exact_matches = {
|
|
# Docker patterns
|
|
"platforms": "docker_architectures",
|
|
"architectures": "docker_architectures",
|
|
"cache_from": "cache_mode",
|
|
"cache_to": "cache_mode",
|
|
"sbom": "sbom_format",
|
|
"registry": "registry_url",
|
|
"registry_url": "registry_url",
|
|
"tags": "docker_tags",
|
|
# File patterns
|
|
"file": "file_path",
|
|
"path": "file_path",
|
|
"file_path": "file_path",
|
|
"config_file": "file_path",
|
|
"dockerfile": "file_path",
|
|
"branch": "branch_name",
|
|
"branch_name": "branch_name",
|
|
"ref": "branch_name",
|
|
# Network patterns
|
|
"email": "email",
|
|
"url": "url",
|
|
"endpoint": "url",
|
|
"webhook": "url",
|
|
"repository_url": "repository_url",
|
|
"repo_url": "repository_url",
|
|
"scope": "scope",
|
|
"username": "username",
|
|
"user": "username",
|
|
# Boolean patterns
|
|
"dry_run": "boolean",
|
|
"draft": "boolean",
|
|
"prerelease": "boolean",
|
|
"push": "boolean",
|
|
"delete": "boolean",
|
|
"all_files": "boolean",
|
|
"force": "boolean",
|
|
"skip": "boolean",
|
|
"enabled": "boolean",
|
|
"disabled": "boolean",
|
|
"verbose": "boolean",
|
|
"debug": "boolean",
|
|
# Numeric patterns
|
|
"retries": "retries",
|
|
"retry": "retries",
|
|
"attempts": "retries",
|
|
"timeout": "timeout",
|
|
"timeout_ms": "timeout",
|
|
"timeout_seconds": "timeout",
|
|
"threads": "threads",
|
|
"workers": "threads",
|
|
"concurrency": "threads",
|
|
# Other patterns
|
|
"category": "category_format",
|
|
"cache": "package_manager_enum",
|
|
"package_manager": "package_manager_enum",
|
|
"format": "report_format",
|
|
"output_format": "report_format",
|
|
"report_format": "report_format",
|
|
"mode": "mode_enum",
|
|
}
|
|
return exact_matches.get(name_lower)
|
|
|
|
def _check_pattern_based_matches(self, name_lower: str) -> str | None: # noqa: PLR0912
|
|
"""Check for pattern-based matches."""
|
|
result = None
|
|
|
|
# Token patterns
|
|
if "token" in name_lower:
|
|
token_types = TOKEN_TYPES
|
|
for key, value in token_types.items():
|
|
if key in name_lower:
|
|
result = value
|
|
break
|
|
if result is None:
|
|
result = "github_token" # Default token type
|
|
|
|
# Docker patterns
|
|
elif name_lower.startswith("docker_"):
|
|
result = f"docker_{name_lower[7:]}"
|
|
|
|
# Version patterns
|
|
elif "version" in name_lower:
|
|
version_mappings = VERSION_MAPPINGS
|
|
for key, value in version_mappings.items():
|
|
if key in name_lower:
|
|
result = value
|
|
break
|
|
if result is None:
|
|
result = "flexible_version" # Default to flexible version
|
|
|
|
# File suffix patterns
|
|
elif name_lower.endswith("_file") and name_lower != "config_file":
|
|
file_types = FILE_TYPES
|
|
for key, value in file_types.items():
|
|
if key in name_lower:
|
|
result = value
|
|
break
|
|
if result is None:
|
|
result = "file_path"
|
|
|
|
# CodeQL patterns
|
|
elif name_lower.startswith("codeql_"):
|
|
result = name_lower
|
|
|
|
# Cache-related check (special case for returning None)
|
|
elif "cache" in name_lower and name_lower != "cache":
|
|
result = None # cache-related but not numeric
|
|
|
|
return result
|
|
|
|
def get_required_inputs(self) -> list[str]:
|
|
"""Get the list of required input names from rules.
|
|
|
|
Returns:
|
|
List of required input names
|
|
"""
|
|
return self._rules.get("required_inputs", [])
|
|
|
|
def get_validation_rules(self) -> dict[str, Any]:
|
|
"""Get the validation rules.
|
|
|
|
Returns:
|
|
Dictionary of validation rules
|
|
"""
|
|
return self._rules
|
|
|
|
def validate_inputs(self, inputs: dict[str, str]) -> bool:
|
|
"""Validate inputs based on conventions and rules.
|
|
|
|
Args:
|
|
inputs: Dictionary of input names to values
|
|
|
|
Returns:
|
|
True if all inputs are valid, False otherwise
|
|
"""
|
|
valid = True
|
|
|
|
# First validate required inputs
|
|
valid &= self.validate_required_inputs(inputs)
|
|
|
|
# Get conventions and overrides from rules
|
|
conventions = self._rules.get("conventions", {})
|
|
overrides = self._rules.get("overrides", {})
|
|
optional_inputs = self._rules.get("optional_inputs", [])
|
|
required_inputs = self.get_required_inputs()
|
|
|
|
# Validate each input
|
|
for input_name, value in inputs.items():
|
|
# Skip if explicitly overridden to null
|
|
if input_name in overrides and overrides[input_name] is None:
|
|
continue
|
|
|
|
# Check if input is defined in the action's rules
|
|
is_defined_input = (
|
|
input_name in required_inputs
|
|
or input_name in optional_inputs
|
|
or input_name in conventions
|
|
or input_name in overrides
|
|
)
|
|
|
|
# Skip validation for undefined inputs with empty values
|
|
# This prevents auto-validation of irrelevant inputs from the
|
|
# validate-inputs action's own input list
|
|
if not is_defined_input and (
|
|
not value or (isinstance(value, str) and not value.strip())
|
|
):
|
|
continue
|
|
|
|
# Get validator type from overrides or conventions
|
|
validator_type = self._get_validator_type(input_name, conventions, overrides)
|
|
|
|
if validator_type:
|
|
# Check if this is a required input
|
|
is_required = input_name in required_inputs
|
|
valid &= self._apply_validator(
|
|
input_name, value, validator_type, is_required=is_required
|
|
)
|
|
|
|
return valid
|
|
|
|
def _get_validator_type(
|
|
self,
|
|
input_name: str,
|
|
conventions: dict[str, str],
|
|
overrides: dict[str, str],
|
|
) -> str | None:
|
|
"""Determine the validator type for an input.
|
|
|
|
Args:
|
|
input_name: The name of the input
|
|
conventions: Convention mappings
|
|
overrides: Override mappings
|
|
|
|
Returns:
|
|
The validator type or None if no validator found
|
|
"""
|
|
# Check overrides first
|
|
if input_name in overrides:
|
|
return overrides[input_name]
|
|
|
|
# Check exact convention match
|
|
if input_name in conventions:
|
|
return conventions[input_name]
|
|
|
|
# Check with dash/underscore conversion
|
|
if "_" in input_name:
|
|
dash_version = input_name.replace("_", "-")
|
|
if dash_version in overrides:
|
|
return overrides[dash_version]
|
|
if dash_version in conventions:
|
|
return conventions[dash_version]
|
|
elif "-" in input_name:
|
|
underscore_version = input_name.replace("-", "_")
|
|
if underscore_version in overrides:
|
|
return overrides[underscore_version]
|
|
if underscore_version in conventions:
|
|
return conventions[underscore_version]
|
|
|
|
# Fall back to convention mapper for pattern-based detection
|
|
return self._convention_mapper.get_validator_type(input_name)
|
|
|
|
def _apply_validator(
|
|
self,
|
|
input_name: str,
|
|
value: str,
|
|
validator_type: str,
|
|
*,
|
|
is_required: bool,
|
|
) -> bool:
|
|
"""Apply the appropriate validator to an input value.
|
|
|
|
Args:
|
|
input_name: The name of the input
|
|
value: The value to validate
|
|
validator_type: The type of validator to apply
|
|
is_required: Whether the input is required
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
# Get the validator module and method
|
|
validator_module, method_name = self._get_validator_method(validator_type)
|
|
|
|
if not validator_module:
|
|
# Unknown validator type, skip validation
|
|
return True
|
|
|
|
try:
|
|
# Call the validation method
|
|
if hasattr(validator_module, method_name):
|
|
method = getattr(validator_module, method_name)
|
|
|
|
# Some validators need additional parameters
|
|
if validator_type == "github_token" and method_name == "validate_github_token":
|
|
result = method(value, required=is_required)
|
|
elif "numeric_range" in validator_type:
|
|
# Parse range from validator type
|
|
min_val, max_val = self._parse_numeric_range(validator_type)
|
|
result = method(value, min_val, max_val, input_name)
|
|
else:
|
|
# Standard validation call
|
|
result = method(value, input_name)
|
|
|
|
# Copy errors from the validator module to this validator
|
|
# Skip if validator_module is self (for internal validators)
|
|
if validator_module is not self and hasattr(validator_module, "errors"):
|
|
for error in validator_module.errors:
|
|
if error not in self.errors:
|
|
self.add_error(error)
|
|
# Clear the module's errors after copying
|
|
validator_module.errors = []
|
|
|
|
return result
|
|
# Method not found, skip validation
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.add_error(f"Validation error for {input_name}: {e}")
|
|
return False
|
|
|
|
def _get_validator_method(self, validator_type: str) -> tuple[Any, str]: # noqa: C901, PLR0912
|
|
"""Get the validator module and method name for a validator type.
|
|
|
|
Args:
|
|
validator_type: The validator type string
|
|
|
|
Returns:
|
|
Tuple of (validator_module, method_name)
|
|
"""
|
|
# Lazy import validators to avoid circular dependencies
|
|
|
|
# Token validators
|
|
if validator_type in [
|
|
"github_token",
|
|
"npm_token",
|
|
"docker_token",
|
|
"namespace_with_lookahead",
|
|
]:
|
|
if "token" not in self._validator_modules:
|
|
from . import token
|
|
|
|
self._validator_modules["token"] = token.TokenValidator()
|
|
return self._validator_modules["token"], f"validate_{validator_type}"
|
|
|
|
# Docker validators
|
|
if validator_type.startswith("docker_") or validator_type in [
|
|
"cache_mode",
|
|
"sbom_format",
|
|
"registry_enum",
|
|
]:
|
|
if "docker" not in self._validator_modules:
|
|
from . import docker
|
|
|
|
self._validator_modules["docker"] = docker.DockerValidator()
|
|
if validator_type.startswith("docker_"):
|
|
method = f"validate_{validator_type[7:]}" # Remove "docker_" prefix
|
|
elif validator_type == "registry_enum":
|
|
method = "validate_registry"
|
|
else:
|
|
method = f"validate_{validator_type}"
|
|
return self._validator_modules["docker"], method
|
|
|
|
# Version validators
|
|
if "version" in validator_type or validator_type in ["calver", "semantic", "flexible"]:
|
|
if "version" not in self._validator_modules:
|
|
from . import version
|
|
|
|
self._validator_modules["version"] = version.VersionValidator()
|
|
return self._validator_modules["version"], f"validate_{validator_type}"
|
|
|
|
# File validators
|
|
if validator_type in [
|
|
"file_path",
|
|
"branch_name",
|
|
"file_extensions",
|
|
"yaml_file",
|
|
"json_file",
|
|
"config_file",
|
|
]:
|
|
if "file" not in self._validator_modules:
|
|
from . import file
|
|
|
|
self._validator_modules["file"] = file.FileValidator()
|
|
return self._validator_modules["file"], f"validate_{validator_type}"
|
|
|
|
# Network validators
|
|
if validator_type in [
|
|
"email",
|
|
"url",
|
|
"scope",
|
|
"username",
|
|
"registry_url",
|
|
"repository_url",
|
|
]:
|
|
if "network" not in self._validator_modules:
|
|
from . import network
|
|
|
|
self._validator_modules["network"] = network.NetworkValidator()
|
|
return self._validator_modules["network"], f"validate_{validator_type}"
|
|
|
|
# Boolean validator
|
|
if validator_type == "boolean":
|
|
if "boolean" not in self._validator_modules:
|
|
from . import boolean
|
|
|
|
self._validator_modules["boolean"] = boolean.BooleanValidator()
|
|
return self._validator_modules["boolean"], "validate_boolean"
|
|
|
|
# Numeric validators
|
|
if validator_type.startswith("numeric_range") or validator_type in [
|
|
"retries",
|
|
"timeout",
|
|
"threads",
|
|
]:
|
|
if "numeric" not in self._validator_modules:
|
|
from . import numeric
|
|
|
|
self._validator_modules["numeric"] = numeric.NumericValidator()
|
|
if validator_type.startswith("numeric_range"):
|
|
return self._validator_modules["numeric"], "validate_range"
|
|
return self._validator_modules["numeric"], f"validate_{validator_type}"
|
|
|
|
# Security validators
|
|
if validator_type in ["security_patterns", "injection_patterns", "prefix", "regex_pattern"]:
|
|
if "security" not in self._validator_modules:
|
|
from . import security
|
|
|
|
self._validator_modules["security"] = security.SecurityValidator()
|
|
if validator_type == "prefix":
|
|
# Use no_injection for prefix - checks for injection patterns
|
|
# without character restrictions
|
|
return self._validator_modules["security"], "validate_no_injection"
|
|
return self._validator_modules["security"], f"validate_{validator_type}"
|
|
|
|
# CodeQL validators
|
|
if validator_type.startswith("codeql_") or validator_type in ["category_format"]:
|
|
if "codeql" not in self._validator_modules:
|
|
from . import codeql
|
|
|
|
self._validator_modules["codeql"] = codeql.CodeQLValidator()
|
|
return self._validator_modules["codeql"], f"validate_{validator_type}"
|
|
|
|
# PHP-specific validators
|
|
if validator_type in ["php_extensions", "coverage_driver", "mode_enum"]:
|
|
# Return self for PHP-specific validation methods
|
|
return self, f"_validate_{validator_type}"
|
|
|
|
# Package manager and report format validators
|
|
if validator_type in ["package_manager_enum", "report_format"]:
|
|
# These could be in a separate module, but for now we'll put them in file validator
|
|
if "file" not in self._validator_modules:
|
|
from . import file
|
|
|
|
self._validator_modules["file"] = file.FileValidator()
|
|
# These methods need to be added to file validator or a new module
|
|
return None, ""
|
|
|
|
# Default: no validator
|
|
return None, ""
|
|
|
|
def _parse_numeric_range(self, validator_type: str) -> tuple[int, int]:
|
|
"""Parse min and max values from a numeric_range validator type.
|
|
|
|
Args:
|
|
validator_type: String like "numeric_range_1_100"
|
|
|
|
Returns:
|
|
Tuple of (min_value, max_value)
|
|
"""
|
|
parts = validator_type.split("_")
|
|
if len(parts) >= 4:
|
|
try:
|
|
return int(parts[2]), int(parts[3])
|
|
except ValueError:
|
|
pass
|
|
# Default range
|
|
return 0, 100
|
|
|
|
def _validate_php_extensions(self, value: str, input_name: str) -> bool:
|
|
"""Validate PHP extensions format.
|
|
|
|
Args:
|
|
value: The extensions value (comma-separated list)
|
|
input_name: The input name for error messages
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
import re
|
|
|
|
if not value:
|
|
return True
|
|
|
|
# Check for injection patterns
|
|
if re.search(r"[;&|`$()@#]", value):
|
|
self.add_error(f"Potential injection detected in {input_name}: {value}")
|
|
return False
|
|
|
|
# Check format - should be alphanumeric, underscores, commas, spaces only
|
|
if not re.match(r"^[a-zA-Z0-9_,\s]+$", value):
|
|
self.add_error(f"Invalid format for {input_name}: {value}")
|
|
return False
|
|
|
|
return True
|
|
|
|
def _validate_coverage_driver(self, value: str, input_name: str) -> bool:
|
|
"""Validate coverage driver enum.
|
|
|
|
Args:
|
|
value: The coverage driver value
|
|
input_name: The input name for error messages
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
valid_drivers = ["none", "xdebug", "pcov", "xdebug3"]
|
|
|
|
if value and value not in valid_drivers:
|
|
self.add_error(
|
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_drivers)}"
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def _validate_mode_enum(self, value: str, input_name: str) -> bool:
|
|
"""Validate mode enum for linting actions.
|
|
|
|
Args:
|
|
value: The mode value
|
|
input_name: The input name for error messages
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
valid_modes = ["check", "fix"]
|
|
|
|
if value and value not in valid_modes:
|
|
self.add_error(
|
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_modes)}"
|
|
)
|
|
return False
|
|
|
|
return True
|