Skip to content

Commit

Permalink
Add a script to validate inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
autarch committed Feb 11, 2025
1 parent 3eede93 commit f20784d
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 11 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,13 @@ jobs:
--github-token "${{ github.token }}" \
--repo houseabsolute/actions-rust-release \
--target "${{ matrix.platform.target }}"
test-validate-inputs:
name: Test validate-inputs
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run tests
shell: bash
run: ./validate-inputs.py --test
26 changes: 15 additions & 11 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,28 @@ outputs:
runs:
using: composite
steps:
- name: Show config
- name: Show inputs
shell: bash
run: |
echo "release-tag-prefix = ${{ inputs.release-tag-prefix }}"
echo "executable-name = ${{ inputs.executable-name }}"
echo "target = ${{ inputs.target }}"
echo "archive-name = ${{ inputs.archive-name }}"
echo "extra-files = ${{ inputs.extra-files }}"
echo "changes-file = ${{ inputs.changes-file }}"
echo "working-directory = ${{ inputs.working-directory }}"
echo "github.ref = ${{ github.ref }}"
echo "github.ref_type = ${{ github.ref_type }}"
echo "matches release-tag-prefix = ${{ startsWith( github.ref_name, inputs.release-tag-prefix ) }}"
echo '${{ toJSON(inputs) }}'
- name: Add this action's path to PATH
shell: bash
run: echo "${{ github.action_path }}" >> $GITHUB_PATH

- name: Validate inputs
shell: bash
run: |
validate-inputs.py "${{ github.workspace }}"
env:
INPUTS_executable_name: ${{ inputs.executable_name }}
INPUTS_release_tag_prefix: ${{ inputs.release-tag-prefix }}
INPUTS_target: ${{ inputs.target }}
INPUTS_archive_name: ${{ inputs.archive-name }}
INPUTS_extra-files: ${{ inputs.extra-files }}
INPUTS_changes_file: ${{ inputs.changes-file }}
INPUTS_working_directory: ${{ inputs.working-directory }}

- name: Package as archive
id: package-archive
shell: bash
Expand Down
282 changes: 282 additions & 0 deletions validate-inputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
#!/usr/bin/env python3

import os
import json
from pathlib import Path
from typing import Dict, List, Union
import tempfile
import unittest


def main() -> None:
"""Main function for running the validator."""
import sys

validator = InputValidator(sys.argv[1])
errors = validator.validate()

if not errors:
print("All inputs are valid.")
sys.exit(0)
else:
for error in errors:
print(error, file=sys.stderr)
sys.exit(1)


class InputValidator:
"""Validate inputs for a GitHub Action that handles Rust binary releases."""

def __init__(self, repo_root: Union[str, Path]):
"""
Create a new InputValidator by collecting environment variables.
Args:
repo_root: Path to the repository root
"""
self.repo_root = Path(repo_root)
self.inputs: Dict[str, str] = {
key.replace("INPUTS_", "").lower(): value
for key, value in os.environ.items()
if key.startswith("INPUTS_")
}

def validate(self) -> List[str]:
"""
Validate all inputs according to specifications.
Returns:
List of validation errors. Empty list means all inputs are valid.
"""
validation_errors: List[str] = []

# Check for required executable-name parameter
if "executable_name" not in self.inputs:
validation_errors.append("'executable-name' is a required parameter")

# Validate that either target or archive-name is present
if "target" not in self.inputs and "archive_name" not in self.inputs:
validation_errors.append(
"Either 'target' or 'archive-name' must be provided"
)

# Validate release-tag-prefix if present
if (
"release_tag_prefix" in self.inputs
and not self.inputs["release_tag_prefix"]
):
validation_errors.append("'release-tag-prefix' cannot be empty if provided")

# Validate working directory if present
working_dir = self.inputs.get("working_directory", ".")
path = Path(working_dir)
if not path.is_absolute():
path = self.repo_root / path

if not path.exists():
validation_errors.append(
f"'working-directory' does not exist: {working_dir}"
)
elif not path.is_dir():
validation_errors.append(
f"'working-directory' is not a directory: {working_dir}"
)

# Validate changes file exists and is a file
changes_file = self.inputs.get("changes_file", "Changes.md")
changes_path = path / changes_file
if not changes_path.exists():
validation_errors.append(
f"Changes file '{changes_file}' not found in working directory"
)
elif not changes_path.is_file():
validation_errors.append(
f"Changes file '{changes_file}' exists but is not a regular file"
)

# Validate extra-files if present
if "extra_files" in self.inputs:
validation_errors.extend(self.validate_extra_files())

# Validate action-gh-release-parameters JSON if present
if "action_gh_release_parameters" in self.inputs:
try:
json.loads(self.inputs["action_gh_release_parameters"])
except json.JSONDecodeError:
validation_errors.append(
"'action-gh-release-parameters' must be valid JSON"
)

return validation_errors

def validate_extra_files(self) -> List[str]:
validation_errors: List[str] = []

extra_files = self.inputs["extra_files"].strip()
if extra_files:
files = [f.strip() for f in extra_files.split("\n")]
if not files:
validation_errors.append("'extra-files' is empty")
for file_path in files:
# Empty lines are okay.
if file_path:
path = Path(file_path)
if not path.is_absolute():
path = self.repo_root / path

if "*" in path.name:
matches = list(path.parent.glob(path.name))
if not matches:
validation_errors.append(
f"Extra file '{file_path}' does not match any paths"
)
for match in matches:
if not match.is_file():
validation_errors.append(
f"Extra file '{file_path}' is not a file"
)
else:
if not path.exists():
validation_errors.append(
f"Extra file '{file_path}' does not exist"
)
elif path.is_dir():
validation_errors.append(
f"Extra file '{file_path}' is a directory"
)

return validation_errors


class TestInputValidator(unittest.TestCase):
"""Unit tests for the InputValidator."""

def setUp(self) -> None:
"""Set up test environment with a clean temp directory."""
# Clear any existing INPUTS_ environment variables
for key in list(os.environ.keys()):
if key.startswith("INPUTS_"):
del os.environ[key]

# Create a new temporary directory for each test
self.temp_dir = tempfile.mkdtemp()

# Create a default Changes.md file that most tests need
self.create_file("Changes.md", "# Changes")

def tearDown(self) -> None:
"""Clean up the temporary test directory."""
import shutil

shutil.rmtree(self.temp_dir)

def create_file(self, name: str, content: str = "") -> Path:
"""Create a file in the test directory with given content."""
path = Path(self.temp_dir) / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
return path

def create_directory(self, name: str) -> Path:
"""Create a directory in the test directory."""
path = Path(self.temp_dir) / name
path.mkdir(parents=True, exist_ok=True)
return path

def setup_env(self, inputs: Dict[str, str]) -> None:
"""Set up environment variables for testing."""
for key, value in inputs.items():
env_key = f"INPUTS_{key.upper().replace('-', '_')}"
os.environ[env_key] = value

def test_get_inputs_from_env(self) -> None:
"""Test getting inputs from environment variables."""
inputs = {
"executable-name": "my-app",
"target": "x86_64-unknown-linux-gnu",
"release-tag-prefix": "v",
"working-directory": self.temp_dir,
}
self.setup_env(inputs)

validator = InputValidator(self.temp_dir)
for key, value in validator.inputs.items():
self.assertEqual(value, inputs[key.replace("_", "-")])

def test_validate_missing_executable_name(self) -> None:
"""Test validation with missing executable-name."""
self.setup_env(
{"target": "x86_64-unknown-linux-gnu", "working-directory": self.temp_dir}
)
validator = InputValidator(self.temp_dir)
errors = validator.validate()
self.assertTrue(any("executable-name" in error for error in errors))

def test_validate_missing_target_and_archive_name(self) -> None:
"""Test validation with missing target and archive-name."""
self.setup_env(
{"executable-name": "my-app", "working-directory": self.temp_dir}
)
validator = InputValidator(self.temp_dir)
errors = validator.validate()
self.assertTrue(any("target" in error for error in errors))

def test_validate_working_directory(self) -> None:
"""Test validation of working directory."""
# Test with valid directory
self.setup_env(
{
"executable-name": "my-app",
"target": "x86_64-unknown-linux-gnu",
"working-directory": self.temp_dir,
}
)
validator = InputValidator(self.temp_dir)
errors = validator.validate()
self.assertFalse(errors)

# Test with non-existent directory
self.setup_env(
{
"executable-name": "my-app",
"target": "x86_64-unknown-linux-gnu",
"working-directory": str(Path(self.temp_dir) / "nonexistent"),
}
)
validator = InputValidator(self.temp_dir)
errors = validator.validate()
self.assertTrue(any("does not exist" in error for error in errors))

def test_validate_changes_file(self) -> None:
"""Test validation of changes file."""
# Test with changes file present
self.setup_env(
{
"executable-name": "my-app",
"target": "x86_64-unknown-linux-gnu",
"working-directory": self.temp_dir,
}
)
validator = InputValidator(self.temp_dir)
errors = validator.validate()
self.assertFalse(errors)

# Test with missing changes file
os.unlink(Path(self.temp_dir) / "Changes.md")
self.setup_env(
{
"executable-name": "my-app",
"target": "x86_64-unknown-linux-gnu",
"working-directory": self.temp_dir,
}
)
validator = InputValidator(self.temp_dir)
errors = validator.validate()
self.assertTrue("not found in working directory" in error for error in errors)


if __name__ == "__main__":
if len(os.sys.argv) > 1 and os.sys.argv[1] == "--test":
unittest.main(argv=["unittest"])
else:
main()

0 comments on commit f20784d

Please sign in to comment.