Files
actions/validate-inputs/docs/ACTION_MAINTAINER.md
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

13 KiB

Action Maintainer Guide

Overview

This guide helps action maintainers understand and use the validation system for their GitHub Actions.

Table of Contents

  1. How Validation Works
  2. Using Automatic Validation
  3. Custom Validation
  4. Testing Your Validation
  5. Common Scenarios
  6. Troubleshooting

How Validation Works

Automatic Integration

Your action automatically gets input validation when using validate-inputs:

# In your action.yml
runs:
  using: composite
  steps:
    - name: Validate inputs
      uses: ./validate-inputs
      with:
        action-type: ${{ github.action }}

Validation Flow

  1. Input Collection: All INPUT_* environment variables are collected
  2. Validator Selection: System chooses appropriate validator
  3. Validation Execution: Each input is validated
  4. Error Reporting: Any errors are reported via ::error::
  5. Status Output: Results written to GITHUB_OUTPUT

Using Automatic Validation

Naming Conventions

Name your inputs to get automatic validation:

Input Pattern Validation Type Example
*-token Token validation github-token, npm-token
*-version Version validation node-version, python-version
dry-run, verbose Boolean dry-run: true
max-*, *-limit Numeric range max-retries, rate-limit
*-file, *-path File path config-file, output-path
*-url, webhook-* URL validation api-url, webhook-endpoint

Example Action

name: My Action
description: Example action with automatic validation

inputs:
  github-token: # Automatically validates GitHub token format
    description: GitHub token for API access
    required: true
    default: ${{ github.token }}

  node-version: # Automatically validates version format
    description: Node.js version to use
    required: false
    default: '18'

  max-retries: # Automatically validates numeric range
    description: Maximum number of retries (1-10)
    required: false
    default: '3'

  config-file: # Automatically validates file path
    description: Configuration file path
    required: false
    default: '.config.yml'

  dry-run: # Automatically validates boolean
    description: Run in dry-run mode
    required: false
    default: 'false'

runs:
  using: composite
  steps:
    - uses: ./validate-inputs
      with:
        action-type: ${{ github.action }}

    - run: echo "Inputs validated successfully"
      shell: bash

Validation Rules File

After creating your action, generate validation rules:

# Generate rules for your action
make update-validators

# Or for a specific action
python3 validate-inputs/scripts/update-validators.py --action my-action

This creates my-action/rules.yml:

schema_version: '1.0'
action: my-action
description: Example action with automatic validation
required_inputs:
  - github-token
optional_inputs:
  - node-version
  - max-retries
  - config-file
  - dry-run
conventions:
  github-token: github_token
  node-version: semantic_version
  max-retries: numeric_range_1_10
  config-file: file_path
  dry-run: boolean

Custom Validation

When to Use Custom Validation

Create a custom validator when:

  • You have complex business logic
  • Cross-field validation is needed
  • Special format requirements exist
  • Default validation is insufficient

Creating a Custom Validator

  1. Create CustomValidator.py in your action directory:
#!/usr/bin/env python3
"""Custom validator for my-action."""

from __future__ import annotations
from pathlib import Path
import sys

# Add validate-inputs to path
validate_inputs_path = Path(__file__).parent.parent / "validate-inputs"
sys.path.insert(0, str(validate_inputs_path))

from validators.base import BaseValidator
from validators.version import VersionValidator


class CustomValidator(BaseValidator):
    """Custom validator for my-action."""

    def __init__(self, action_type: str = "my-action") -> None:
        super().__init__(action_type)
        self.version_validator = VersionValidator(action_type)

    def validate_inputs(self, inputs: dict[str, str]) -> bool:
        valid = True

        # Check required inputs
        valid &= self.validate_required_inputs(inputs)

        # Custom validation
        if inputs.get("environment"):
            valid &= self.validate_environment(inputs["environment"])

        # Cross-field validation
        if inputs.get("environment") == "production":
            if not inputs.get("approval-required"):
                self.add_error(
                    "Production deployments require approval-required=true"
                )
                valid = False

        return valid

    def get_required_inputs(self) -> list[str]:
        return ["environment", "target"]

    def validate_environment(self, env: str) -> bool:
        valid_envs = ["development", "staging", "production"]
        if env not in valid_envs:
            self.add_error(
                f"Invalid environment: {env}. "
                f"Must be one of: {', '.join(valid_envs)}"
            )
            return False
        return True

    def get_validation_rules(self) -> dict:
        """Get validation rules."""
        rules_path = Path(__file__).parent / "rules.yml"
        return self.load_rules(rules_path)
  1. Test your validator (optional but recommended):
# my-action/test_custom_validator.py
from CustomValidator import CustomValidator

def test_valid_inputs():
    validator = CustomValidator()
    inputs = {
        "environment": "production",
        "target": "app-server",
        "approval-required": "true"
    }
    assert validator.validate_inputs(inputs) is True
    assert len(validator.errors) == 0

Testing Your Validation

Manual Testing

# Test with environment variables
export INPUT_ACTION_TYPE="my-action"
export INPUT_GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}"
export INPUT_NODE_VERSION="18.0.0"
export INPUT_DRY_RUN="true"

python3 validate-inputs/validator.py

Integration Testing

Create a test workflow:

# .github/workflows/test-my-action.yml
name: Test My Action Validation

on:
  pull_request:
    paths:
      - 'my-action/**'
      - 'validate-inputs/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Test valid inputs
      - name: Test with valid inputs
        uses: ./my-action
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          node-version: '18.0.0'
          dry-run: 'true'

      # Test invalid inputs (should fail)
      - name: Test with invalid inputs
        id: invalid
        continue-on-error: true
        uses: ./my-action
        with:
          github-token: 'invalid-token'
          node-version: 'not-a-version'
          dry-run: 'maybe'

      - name: Check failure
        if: steps.invalid.outcome != 'failure'
        run: exit 1

Generating Tests

Use the test generator:

# Generate tests for your action
make generate-tests

# Preview what would be generated
make generate-tests-dry

# Run the generated tests
make test

Common Scenarios

Scenario 1: Required Inputs

inputs:
  api-key:
    description: API key for service
    required: true # No default value

Validation automatically enforces this requirement.

Scenario 2: Dependent Inputs

Use custom validator for dependent fields:

def validate_inputs(self, inputs: dict[str, str]) -> bool:
    # If using custom registry, token is required
    if inputs.get("registry") and not inputs.get("registry-token"):
        self.add_error("registry-token required when using custom registry")
        return False
    return True

Scenario 3: Complex Formats

def validate_cron_schedule(self, schedule: str) -> bool:
    """Validate cron schedule format."""
    import re

    # Simple cron pattern (not exhaustive)
    pattern = r'^(\*|[0-9,\-\*/]+)\s+(\*|[0-9,\-\*/]+)\s+(\*|[0-9,\-\*/]+)\s+(\*|[0-9,\-\*/]+)\s+(\*|[0-9,\-\*/]+)$'

    if not re.match(pattern, schedule):
        self.add_error(f"Invalid cron schedule: {schedule}")
        return False
    return True

Scenario 4: External Service Validation

def validate_docker_image_exists(self, image: str) -> bool:
    """Check if Docker image exists (example)."""
    # Note: Be careful with external calls in validation
    # Consider caching or making this optional

    # Allow GitHub Actions expressions
    if self.is_github_expression(image):
        return True

    # Simplified check - real implementation would need error handling
    import subprocess
    result = subprocess.run(
        ["docker", "manifest", "inspect", image],
        capture_output=True,
        text=True
    )

    if result.returncode != 0:
        self.add_error(f"Docker image not found: {image}")
        return False
    return True

Troubleshooting

Issue: Validation Not Running

Check:

  1. Is validate-inputs action called in your workflow?
  2. Is action-type parameter set correctly?
  3. Are environment variables prefixed with INPUT_?

Debug:

- name: Debug inputs
  run: |
    env | grep INPUT_ | sort
  shell: bash

- uses: ./validate-inputs
  with:
    action-type: ${{ github.action }}

Issue: Custom Validator Not Found

Check:

  1. Is CustomValidator.py in action directory?
  2. Is class named exactly CustomValidator?
  3. Is file readable and valid Python?

Debug:

# Test import directly
python3 -c "from my_action.CustomValidator import CustomValidator; print('Success')"

Issue: Validation Too Strict

Solutions:

  1. Allow GitHub expressions:
if self.is_github_expression(value):
    return True
  1. Make fields optional:
if not value or not value.strip():
    return True  # Empty is OK for optional fields
  1. Add to allowed values:
valid_values = ["option1", "option2", "custom"]  # Add more options

Issue: Validation Not Strict Enough

Solutions:

  1. Create custom validator with stricter rules
  2. Add pattern matching:
import re
if not re.match(r'^[a-z0-9\-]+$', value):
    self.add_error("Only lowercase letters, numbers, and hyphens allowed")
  1. Add length limits:
if len(value) > 100:
    self.add_error("Value too long (max 100 characters)")

Getting Validation Status

Access validation results in subsequent steps:

- uses: ./validate-inputs
  id: validation
  with:
    action-type: my-action

- name: Check validation status
  run: |
    echo "Status: ${{ steps.validation.outputs.status }}"
    echo "Valid: ${{ steps.validation.outputs.valid }}"
    echo "Action: ${{ steps.validation.outputs.action }}"
    echo "Inputs validated: ${{ steps.validation.outputs.inputs_validated }}"
  shell: bash

Debugging Validation Errors

Enable debug output:

- uses: ./validate-inputs
  with:
    action-type: my-action
  env:
    ACTIONS_RUNNER_DEBUG: true
    ACTIONS_STEP_DEBUG: true

View specific errors:

# In your action
- name: Validate
  id: validate
  uses: ./validate-inputs
  continue-on-error: true
  with:
    action-type: my-action

- name: Show errors
  if: steps.validate.outcome == 'failure'
  run: |
    echo "Validation failed!"
    # Errors are already shown via ::error::
  shell: bash

Best Practices

  1. Use conventions when possible for automatic validation
  2. Document validation rules in your action's README
  3. Test with invalid inputs to ensure validation works
  4. Allow GitHub expressions (${{ }}) in all validators
  5. Provide clear error messages that explain how to fix the issue
  6. Make validation fast - avoid expensive operations
  7. Cache validation results if checking external resources
  8. Version your validation - use validate-inputs@v1 etc.
  9. Monitor validation failures in your action's usage

Resources

Support

For validation issues:

  1. Check error messages for specific problems
  2. Review validation rules in action folder's rules.yml
  3. Test with simplified inputs
  4. Create custom validator if needed
  5. Report bugs via GitHub Issues