mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +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
666 lines
18 KiB
Markdown
666 lines
18 KiB
Markdown
# GitHub Actions Testing Framework
|
|
|
|
A comprehensive testing framework for validating GitHub Actions in this monorepo using ShellSpec and Python-based input validation.
|
|
|
|
## 🚀 Quick Start
|
|
|
|
```bash
|
|
# Run all tests
|
|
make test
|
|
|
|
# Run only unit tests
|
|
make test-unit
|
|
|
|
# Run tests for specific action
|
|
make test-action ACTION=node-setup
|
|
|
|
# Run with coverage reporting
|
|
make test-coverage
|
|
```
|
|
|
|
### Prerequisites
|
|
|
|
```bash
|
|
# Install ShellSpec (testing framework)
|
|
curl -fsSL https://github.com/shellspec/shellspec/releases/latest/download/shellspec-dist.tar.gz | tar -xz
|
|
sudo make -C shellspec-* install
|
|
|
|
# Install nektos/act (optional, for integration tests)
|
|
brew install act # macOS
|
|
# or: curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
|
|
```
|
|
|
|
## 📁 Framework Overview
|
|
|
|
### Architecture
|
|
|
|
The testing framework uses a **multi-level testing strategy**:
|
|
|
|
1. **Unit Tests** - Fast validation of action logic, inputs, and outputs using Python validation
|
|
2. **Integration Tests** - Test actions in realistic workflow environments
|
|
3. **External Usage Tests** - Validate actions work as `ivuorinen/actions/action-name@main`
|
|
|
|
### Technology Stack
|
|
|
|
- **Primary Framework**: [ShellSpec](https://shellspec.info/) - BDD testing for shell scripts
|
|
- **Validation**: Python-based input validation via `validate-inputs/validator.py`
|
|
- **Local Execution**: [nektos/act](https://github.com/nektos/act) - Run GitHub Actions locally
|
|
- **CI Integration**: GitHub Actions workflows
|
|
|
|
### Directory Structure
|
|
|
|
```text
|
|
_tests/
|
|
├── README.md # This documentation
|
|
├── run-tests.sh # Main test runner script
|
|
├── unit/ # Unit tests by action
|
|
│ ├── spec_helper.sh # ShellSpec helper with validation functions
|
|
│ ├── version-file-parser/ # Example unit tests
|
|
│ ├── node-setup/ # Example unit tests
|
|
│ └── ... # One directory per action
|
|
├── framework/ # Core testing utilities
|
|
│ ├── setup.sh # Test environment setup
|
|
│ ├── utils.sh # Common testing functions
|
|
│ ├── validation.py # Python validation utilities
|
|
│ └── fixtures/ # Test fixtures
|
|
├── integration/ # Integration tests
|
|
│ ├── workflows/ # Test workflows for nektos/act
|
|
│ ├── external-usage/ # External reference tests
|
|
│ └── action-chains/ # Multi-action workflow tests
|
|
├── coverage/ # Coverage reports
|
|
└── reports/ # Test execution reports
|
|
```
|
|
|
|
## ✍️ Writing Tests
|
|
|
|
### Basic Unit Test Structure
|
|
|
|
```bash
|
|
#!/usr/bin/env shellspec
|
|
# _tests/unit/my-action/validation.spec.sh
|
|
|
|
Describe "my-action validation"
|
|
ACTION_DIR="my-action"
|
|
ACTION_FILE="$ACTION_DIR/action.yml"
|
|
|
|
Context "when validating required inputs"
|
|
It "accepts valid input"
|
|
When call validate_input_python "my-action" "input-name" "valid-value"
|
|
The status should be success
|
|
End
|
|
|
|
It "rejects invalid input"
|
|
When call validate_input_python "my-action" "input-name" "invalid@value"
|
|
The status should be failure
|
|
End
|
|
End
|
|
|
|
Context "when validating boolean inputs"
|
|
It "accepts true"
|
|
When call validate_input_python "my-action" "dry-run" "true"
|
|
The status should be success
|
|
End
|
|
|
|
It "accepts false"
|
|
When call validate_input_python "my-action" "dry-run" "false"
|
|
The status should be success
|
|
End
|
|
|
|
It "rejects invalid boolean"
|
|
When call validate_input_python "my-action" "dry-run" "maybe"
|
|
The status should be failure
|
|
End
|
|
End
|
|
End
|
|
```
|
|
|
|
### Integration Test Example
|
|
|
|
```yaml
|
|
# _tests/integration/workflows/my-action-test.yml
|
|
name: Test my-action Integration
|
|
on: workflow_dispatch
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Test action locally
|
|
id: test-local
|
|
uses: ./my-action
|
|
with:
|
|
required-input: 'test-value'
|
|
|
|
- name: Validate outputs
|
|
run: |
|
|
echo "Output: ${{ steps.test-local.outputs.result }}"
|
|
[[ -n "${{ steps.test-local.outputs.result }}" ]] || exit 1
|
|
|
|
- name: Test external reference
|
|
uses: ivuorinen/actions/my-action@main
|
|
with:
|
|
required-input: 'test-value'
|
|
```
|
|
|
|
## 🛠️ Testing Functions
|
|
|
|
### Primary Validation Function
|
|
|
|
The framework provides one main validation function that uses the Python validation system:
|
|
|
|
#### validate_input_python
|
|
|
|
Tests input validation using the centralized Python validator:
|
|
|
|
```bash
|
|
validate_input_python "action-name" "input-name" "test-value"
|
|
```
|
|
|
|
**Examples:**
|
|
|
|
```bash
|
|
# Boolean validation
|
|
validate_input_python "pre-commit" "dry-run" "true" # success
|
|
validate_input_python "pre-commit" "dry-run" "false" # success
|
|
validate_input_python "pre-commit" "dry-run" "maybe" # failure
|
|
|
|
# Version validation
|
|
validate_input_python "node-setup" "node-version" "18.0.0" # success
|
|
validate_input_python "node-setup" "node-version" "v1.2.3" # success
|
|
validate_input_python "node-setup" "node-version" "invalid" # failure
|
|
|
|
# Token validation
|
|
validate_input_python "npm-publish" "npm-token" "ghp_123..." # success
|
|
validate_input_python "npm-publish" "npm-token" "invalid" # failure
|
|
|
|
# Docker validation
|
|
validate_input_python "docker-build" "image-name" "myapp" # success
|
|
validate_input_python "docker-build" "tag" "v1.0.0" # success
|
|
|
|
# Path validation (security)
|
|
validate_input_python "pre-commit" "config-file" "config.yml" # success
|
|
validate_input_python "pre-commit" "config-file" "../etc/pass" # failure
|
|
|
|
# Injection detection
|
|
validate_input_python "common-retry" "command" "echo test" # success
|
|
validate_input_python "common-retry" "command" "rm -rf /; " # failure
|
|
```
|
|
|
|
### Helper Functions from spec_helper.sh
|
|
|
|
```bash
|
|
# Setup/cleanup
|
|
setup_default_inputs "action-name" "input-name" # Set required defaults
|
|
cleanup_default_inputs "action-name" "input-name" # Clean up defaults
|
|
shellspec_setup_test_env "test-name" # Setup test environment
|
|
shellspec_cleanup_test_env "test-name" # Cleanup test environment
|
|
|
|
# Mock execution
|
|
shellspec_mock_action_run "action-dir" key1 value1 key2 value2
|
|
shellspec_validate_action_output "expected-key" "expected-value"
|
|
|
|
# Action metadata
|
|
validate_action_yml "action.yml" # Validate YAML structure
|
|
get_action_inputs "action.yml" # Get action inputs
|
|
get_action_outputs "action.yml" # Get action outputs
|
|
get_action_name "action.yml" # Get action name
|
|
```
|
|
|
|
### Complete Action Validation Example
|
|
|
|
```bash
|
|
Describe "comprehensive-action validation"
|
|
ACTION_DIR="comprehensive-action"
|
|
ACTION_FILE="$ACTION_DIR/action.yml"
|
|
|
|
Context "when validating all input types"
|
|
It "validates boolean inputs"
|
|
When call validate_input_python "$ACTION_DIR" "verbose" "true"
|
|
The status should be success
|
|
|
|
When call validate_input_python "$ACTION_DIR" "verbose" "false"
|
|
The status should be success
|
|
|
|
When call validate_input_python "$ACTION_DIR" "verbose" "invalid"
|
|
The status should be failure
|
|
End
|
|
|
|
It "validates numeric inputs"
|
|
When call validate_input_python "$ACTION_DIR" "max-retries" "3"
|
|
The status should be success
|
|
|
|
When call validate_input_python "$ACTION_DIR" "max-retries" "999"
|
|
The status should be failure
|
|
End
|
|
|
|
It "validates version inputs"
|
|
When call validate_input_python "$ACTION_DIR" "tool-version" "1.0.0"
|
|
The status should be success
|
|
|
|
When call validate_input_python "$ACTION_DIR" "tool-version" "v1.2.3-rc.1"
|
|
The status should be success
|
|
End
|
|
|
|
It "validates security patterns"
|
|
When call validate_input_python "$ACTION_DIR" "command" "echo test"
|
|
The status should be success
|
|
|
|
When call validate_input_python "$ACTION_DIR" "command" "rm -rf /; "
|
|
The status should be failure
|
|
End
|
|
End
|
|
|
|
Context "when validating action structure"
|
|
It "has valid YAML structure"
|
|
When call validate_action_yml "$ACTION_FILE"
|
|
The status should be success
|
|
End
|
|
End
|
|
End
|
|
```
|
|
|
|
## 🎯 Testing Patterns by Action Type
|
|
|
|
### Setup Actions (node-setup, php-version-detect, etc.)
|
|
|
|
Focus on version detection and environment setup:
|
|
|
|
```bash
|
|
Context "when detecting versions"
|
|
It "detects version from config files"
|
|
When call validate_input_python "node-setup" "node-version" "18.0.0"
|
|
The status should be success
|
|
End
|
|
|
|
It "accepts default version"
|
|
When call validate_input_python "python-version-detect" "default-version" "3.11"
|
|
The status should be success
|
|
End
|
|
End
|
|
```
|
|
|
|
### Linting Actions (eslint-fix, prettier-fix, etc.)
|
|
|
|
Focus on file processing and security:
|
|
|
|
```bash
|
|
Context "when processing files"
|
|
It "validates working directory"
|
|
When call validate_input_python "eslint-fix" "working-directory" "."
|
|
The status should be success
|
|
End
|
|
|
|
It "rejects path traversal"
|
|
When call validate_input_python "eslint-fix" "working-directory" "../etc"
|
|
The status should be failure
|
|
End
|
|
|
|
It "validates boolean flags"
|
|
When call validate_input_python "eslint-fix" "fix-only" "true"
|
|
The status should be success
|
|
End
|
|
End
|
|
```
|
|
|
|
### Build Actions (docker-build, go-build, etc.)
|
|
|
|
Focus on build configuration:
|
|
|
|
```bash
|
|
Context "when building"
|
|
It "validates image name"
|
|
When call validate_input_python "docker-build" "image-name" "myapp"
|
|
The status should be success
|
|
End
|
|
|
|
It "validates tag format"
|
|
When call validate_input_python "docker-build" "tag" "v1.0.0"
|
|
The status should be success
|
|
End
|
|
|
|
It "validates platforms"
|
|
When call validate_input_python "docker-build" "platforms" "linux/amd64,linux/arm64"
|
|
The status should be success
|
|
End
|
|
End
|
|
```
|
|
|
|
### Publishing Actions (npm-publish, docker-publish, etc.)
|
|
|
|
Focus on credentials and registry validation:
|
|
|
|
```bash
|
|
Context "when publishing"
|
|
It "validates token format"
|
|
When call validate_input_python "npm-publish" "npm-token" "ghp_123456789012345678901234567890123456"
|
|
The status should be success
|
|
End
|
|
|
|
It "rejects invalid token"
|
|
When call validate_input_python "npm-publish" "npm-token" "invalid-token"
|
|
The status should be failure
|
|
End
|
|
|
|
It "validates version"
|
|
When call validate_input_python "npm-publish" "package-version" "1.0.0"
|
|
The status should be success
|
|
End
|
|
End
|
|
```
|
|
|
|
## 🔧 Running Tests
|
|
|
|
### Command Line Interface
|
|
|
|
```bash
|
|
# Basic usage
|
|
./_tests/run-tests.sh [OPTIONS] [ACTION_NAME...]
|
|
|
|
# Examples
|
|
./_tests/run-tests.sh # All tests, all actions
|
|
./_tests/run-tests.sh -t unit # Unit tests only
|
|
./_tests/run-tests.sh -a node-setup # Specific action
|
|
./_tests/run-tests.sh -t integration docker-build # Integration tests for docker-build
|
|
./_tests/run-tests.sh --format json --coverage # JSON output with coverage
|
|
```
|
|
|
|
### Options
|
|
|
|
| Option | Description |
|
|
|-----------------------|------------------------------------------------|
|
|
| `-t, --type TYPE` | Test type: `unit`, `integration`, `e2e`, `all` |
|
|
| `-a, --action ACTION` | Filter by action name pattern |
|
|
| `-j, --jobs JOBS` | Number of parallel jobs (default: 4) |
|
|
| `-c, --coverage` | Enable coverage reporting |
|
|
| `-f, --format FORMAT` | Output format: `console`, `json`, `junit` |
|
|
| `-v, --verbose` | Enable verbose output |
|
|
| `-h, --help` | Show help message |
|
|
|
|
### Make Targets
|
|
|
|
```bash
|
|
make test # Run all tests
|
|
make test-unit # Unit tests only
|
|
make test-integration # Integration tests only
|
|
make test-coverage # Tests with coverage
|
|
make test-action ACTION=name # Test specific action
|
|
```
|
|
|
|
## 🤝 Contributing Tests
|
|
|
|
### Adding Tests for New Actions
|
|
|
|
1. **Create Unit Test Directory**
|
|
|
|
```bash
|
|
mkdir -p _tests/unit/new-action
|
|
```
|
|
|
|
2. **Write Unit Tests**
|
|
|
|
```bash
|
|
# _tests/unit/new-action/validation.spec.sh
|
|
#!/usr/bin/env shellspec
|
|
|
|
Describe "new-action validation"
|
|
ACTION_DIR="new-action"
|
|
ACTION_FILE="$ACTION_DIR/action.yml"
|
|
|
|
Context "when validating inputs"
|
|
It "validates required input"
|
|
When call validate_input_python "new-action" "required-input" "value"
|
|
The status should be success
|
|
End
|
|
End
|
|
End
|
|
```
|
|
|
|
3. **Create Integration Test**
|
|
|
|
```bash
|
|
# _tests/integration/workflows/new-action-test.yml
|
|
# (See integration test example above)
|
|
```
|
|
|
|
4. **Test Your Tests**
|
|
|
|
```bash
|
|
make test-action ACTION=new-action
|
|
```
|
|
|
|
### Pull Request Checklist
|
|
|
|
- [ ] Tests use `validate_input_python` for input validation
|
|
- [ ] All test types pass locally (`make test`)
|
|
- [ ] Integration test workflow created
|
|
- [ ] Security testing included for user inputs
|
|
- [ ] Tests are independent and isolated
|
|
- [ ] Proper cleanup in test teardown
|
|
- [ ] Documentation updated if needed
|
|
|
|
## 💡 Best Practices
|
|
|
|
### 1. Use validate_input_python for All Input Testing
|
|
|
|
✅ **Good**:
|
|
|
|
```bash
|
|
When call validate_input_python "my-action" "verbose" "true"
|
|
The status should be success
|
|
```
|
|
|
|
❌ **Avoid**:
|
|
|
|
```bash
|
|
# Don't manually test validation - use the Python validator
|
|
export INPUT_VERBOSE="true"
|
|
python3 validate-inputs/validator.py
|
|
```
|
|
|
|
### 2. Group Related Validations
|
|
|
|
✅ **Good**:
|
|
|
|
```bash
|
|
Context "when validating configuration"
|
|
It "accepts valid boolean"
|
|
When call validate_input_python "my-action" "dry-run" "true"
|
|
The status should be success
|
|
End
|
|
|
|
It "accepts valid version"
|
|
When call validate_input_python "my-action" "tool-version" "1.0.0"
|
|
The status should be success
|
|
End
|
|
End
|
|
```
|
|
|
|
### 3. Always Include Security Testing
|
|
|
|
✅ **Always include**:
|
|
|
|
```bash
|
|
It "rejects command injection"
|
|
When call validate_input_python "common-retry" "command" "rm -rf /; "
|
|
The status should be failure
|
|
End
|
|
|
|
It "rejects path traversal"
|
|
When call validate_input_python "pre-commit" "config-file" "../etc/passwd"
|
|
The status should be failure
|
|
End
|
|
```
|
|
|
|
### 4. Write Descriptive Test Names
|
|
|
|
✅ **Good**:
|
|
|
|
```bash
|
|
It "accepts valid semantic version format"
|
|
It "rejects version with invalid characters"
|
|
It "falls back to default when no version file exists"
|
|
```
|
|
|
|
❌ **Avoid**:
|
|
|
|
```bash
|
|
It "validates input"
|
|
It "works correctly"
|
|
```
|
|
|
|
### 5. Keep Tests Independent
|
|
|
|
- Each test should work in isolation
|
|
- Don't rely on test execution order
|
|
- Clean up after each test
|
|
- Use proper setup/teardown
|
|
|
|
## 🔍 Framework Features
|
|
|
|
### Test Environment Setup
|
|
|
|
The framework automatically sets up test environments via `spec_helper.sh`:
|
|
|
|
```bash
|
|
# Automatic setup on load
|
|
- GitHub Actions environment variables
|
|
- Temporary directories
|
|
- Mock GITHUB_OUTPUT files
|
|
- Default required inputs for actions
|
|
|
|
# Available variables
|
|
$PROJECT_ROOT # Repository root
|
|
$TEST_ROOT # _tests/ directory
|
|
$FRAMEWORK_DIR # _tests/framework/
|
|
$FIXTURES_DIR # _tests/framework/fixtures/
|
|
$TEMP_DIR # Temporary test directory
|
|
$GITHUB_OUTPUT # Mock outputs file
|
|
$GITHUB_ENV # Mock environment file
|
|
```
|
|
|
|
### Python Validation Integration
|
|
|
|
All input validation uses the centralized Python validation system from `validate-inputs/`:
|
|
|
|
- Convention-based automatic validation
|
|
- 9 specialized validators (Boolean, Version, Token, Numeric, File, Network, Docker, Security, CodeQL)
|
|
- Custom validator support per action
|
|
- Injection and security pattern detection
|
|
|
|
## 🚨 Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### "ShellSpec command not found"
|
|
|
|
```bash
|
|
# Install ShellSpec globally
|
|
curl -fsSL https://github.com/shellspec/shellspec/releases/latest/download/shellspec-dist.tar.gz | tar -xz
|
|
sudo make -C shellspec-* install
|
|
```
|
|
|
|
#### "act command not found"
|
|
|
|
```bash
|
|
# Install nektos/act (macOS)
|
|
brew install act
|
|
|
|
# Install nektos/act (Linux)
|
|
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
|
|
```
|
|
|
|
#### Tests timeout
|
|
|
|
```bash
|
|
# Increase timeout for slow operations
|
|
export TEST_TIMEOUT=300
|
|
```
|
|
|
|
#### Permission denied on test scripts
|
|
|
|
```bash
|
|
# Make test scripts executable
|
|
find _tests/ -name "*.sh" -exec chmod +x {} \;
|
|
```
|
|
|
|
### Debugging Tests
|
|
|
|
1. **Enable Verbose Mode**
|
|
|
|
```bash
|
|
./_tests/run-tests.sh -v
|
|
```
|
|
|
|
2. **Run Single Test**
|
|
|
|
```bash
|
|
shellspec _tests/unit/my-action/validation.spec.sh
|
|
```
|
|
|
|
3. **Enable Debug Mode**
|
|
|
|
```bash
|
|
export SHELLSPEC_DEBUG=1
|
|
shellspec _tests/unit/my-action/validation.spec.sh
|
|
```
|
|
|
|
4. **Check Test Output**
|
|
|
|
```bash
|
|
# Test results stored in _tests/reports/
|
|
cat _tests/reports/unit/my-action.txt
|
|
```
|
|
|
|
## 📚 Resources
|
|
|
|
- [ShellSpec Documentation](https://shellspec.info/)
|
|
- [nektos/act Documentation](https://nektosact.com/)
|
|
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
|
|
- [Testing GitHub Actions Best Practices](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action#testing-your-action)
|
|
- [validate-inputs Documentation](../validate-inputs/docs/README_ARCHITECTURE.md)
|
|
|
|
## Framework Development
|
|
|
|
### Framework File Structure
|
|
|
|
```text
|
|
_tests/
|
|
├── unit/
|
|
│ └── spec_helper.sh # ShellSpec configuration and helpers
|
|
├── framework/
|
|
│ ├── setup.sh # Test environment initialization
|
|
│ ├── utils.sh # Common utility functions
|
|
│ ├── validation.py # Python validation helpers
|
|
│ └── fixtures/ # Test fixtures
|
|
└── integration/
|
|
├── workflows/ # Integration test workflows
|
|
├── external-usage/ # External reference tests
|
|
└── action-chains/ # Multi-action tests
|
|
```
|
|
|
|
### Available Functions
|
|
|
|
**From spec_helper.sh (\_tests/unit/spec_helper.sh):**
|
|
|
|
- `validate_input_python(action, input_name, value)` - Main validation function
|
|
- `setup_default_inputs(action, input_name)` - Set default required inputs
|
|
- `cleanup_default_inputs(action, input_name)` - Clean up default inputs
|
|
- `shellspec_setup_test_env(name)` - Setup test environment
|
|
- `shellspec_cleanup_test_env(name)` - Cleanup test environment
|
|
- `shellspec_mock_action_run(action_dir, ...)` - Mock action execution
|
|
- `shellspec_validate_action_output(key, value)` - Validate outputs
|
|
|
|
**From utils.sh (\_tests/framework/utils.sh):**
|
|
|
|
- `validate_action_yml(file)` - Validate action YAML
|
|
- `get_action_inputs(file)` - Extract action inputs
|
|
- `get_action_outputs(file)` - Extract action outputs
|
|
- `get_action_name(file)` - Get action name
|
|
- `test_input_validation(dir, name, value, expected)` - Test input
|
|
- `test_action_outputs(dir)` - Test action outputs
|
|
- `test_external_usage(dir)` - Test external usage
|
|
|
|
**Last Updated:** October 15, 2025
|