Validate inputs to this action

This commit is contained in:
Dave Rolsky 2024-12-14 15:48:06 -06:00
parent 02640563b4
commit dad7ec15de
No known key found for this signature in database
5 changed files with 311 additions and 8 deletions

View File

@ -5,8 +5,8 @@ on:
pull_request: pull_request:
jobs: jobs:
test: test-action:
name: Test name: Test action
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -291,3 +291,13 @@ jobs:
--expect-cross-version "${{ matrix.platform.expect_cross_version }}" \ --expect-cross-version "${{ matrix.platform.expect_cross_version }}" \
${{ matrix.platform.expect_cross }} \ ${{ matrix.platform.expect_cross }} \
${{ matrix.platform.expect_stripped }} ${{ matrix.platform.expect_stripped }}
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

View File

@ -6,6 +6,7 @@ bumped to v1.0.0 because of this change.
- This action will now configure and use `Swatinem/rust-cache` by default for you. It will include - This action will now configure and use `Swatinem/rust-cache` by default for you. It will include
the `target` parameter as part of the cache key automatically. Suggested by @jennydaman (Jennings the `target` parameter as part of the cache key automatically. Suggested by @jennydaman (Jennings
Zhang). GH #23. Zhang). GH #23.
- This action now validates its input and will exit early if they are not valid. GH #35.
## 0.0.17 - 2024-11-23 ## 0.0.17 - 2024-11-23

View File

@ -6,20 +6,20 @@ branding:
description: | description: |
Cross compile your Rust projects with cross (https://github.com/cross-rs/cross). Cross compile your Rust projects with cross (https://github.com/cross-rs/cross).
inputs: inputs:
working-directory: target:
description: The working directory for each step description: The target platform
default: "." required: true
command: command:
description: | description: |
The commands to run. This must be one of "build", "test", "both" (build and test), or "bench". The commands to run. This must be one of "build", "test", "both" (build and test), or "bench".
default: build default: build
target:
description: The target platform
required: true
toolchain: toolchain:
description: | description: |
The target toolchain to use (one of "stable", "beta", or "nightly"). The target toolchain to use (one of "stable", "beta", or "nightly").
default: stable default: stable
working-directory:
description: The working directory for each step
default: "."
GITHUB_TOKEN: GITHUB_TOKEN:
description: | description: |
A GitHub token, available in the secrets.GITHUB_TOKEN working-directory variable. A GitHub token, available in the secrets.GITHUB_TOKEN working-directory variable.
@ -56,6 +56,19 @@ runs:
- name: Add this action's path to PATH - name: Add this action's path to PATH
shell: bash shell: bash
run: echo "${{ github.action_path }}" >> $GITHUB_PATH run: echo "${{ github.action_path }}" >> $GITHUB_PATH
- name: Validate inputs
shell: bash
run: |
"${{ github.action_path }}"/validate-inputs.py "${{ github.workspace }}"
env:
INPUTS_target: ${{ inputs.target }}
INPUTS_command: ${{ inputs.command }}
INPUTS_toolchain: ${{ inputs.toolchain }}
INPUTS_working_directory: ${{ inputs.working-directory }}
INPUTS_strip: ${{ inputs.strip }}
INPUTS_cache_cross_binary: ${{ inputs.cache-cross-binary }}
INPUTS_use_rust_cache: ${{ inputs.use-rust-cache }}
INPUTS_rust_cache_parameters: ${{ inputs.rust-cache-parameters }}
- name: Determine whether we need to cross-compile - name: Determine whether we need to cross-compile
id: determine-cross-compile id: determine-cross-compile
shell: bash shell: bash

View File

@ -3,6 +3,24 @@ exclude = [
"tests/lib/**/*", "tests/lib/**/*",
] ]
[commands."ruff for linting"]
type = "both"
include = [ "**/*.py" ]
cmd = "ruff"
lint_flags = [ "check" ]
tidy_flags = [ "check", "--fix" ]
ok_exit_codes = 0
lint_failure_exit_codes = 1
[commands."ruff for tidying"]
type = "both"
include = [ "**/*.py" ]
cmd = "ruff"
lint_flags = [ "format", "--check" ]
tidy_flags = [ "format" ]
ok_exit_codes = 0
lint_failure_exit_codes = 1
[commands.typos] [commands.typos]
type = "both" type = "both"
include = "**/*" include = "**/*"

261
validate-inputs.py Executable file
View File

@ -0,0 +1,261 @@
#!/usr/bin/env python3
# Written by Claude.ai
import os
import json
from pathlib import Path
from typing import Dict, List, Union
import tempfile
import unittest
class InputValidator:
"""Validate inputs for a GitHub Action."""
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 'target' parameter
if "target" not in self.inputs:
validation_errors.append("'target' is a required parameter")
# Validate command if present
if "command" in self.inputs:
valid_commands = {"build", "test", "both", "bench"}
if self.inputs["command"] not in valid_commands:
validation_errors.append(
f"Invalid 'command'. Must be one of {sorted(valid_commands)}"
)
# Validate toolchain if present
if "toolchain" in self.inputs:
valid_toolchains = {"stable", "beta", "nightly"}
if self.inputs["toolchain"] not in valid_toolchains:
validation_errors.append(
f"Invalid 'toolchain'. Must be one of {sorted(valid_toolchains)}"
)
# Validate working directory if present
if "working_directory" in self.inputs:
path = Path(self.inputs["working_directory"])
if not path.is_absolute():
path = self.repo_root / path
if not path.exists():
validation_errors.append(
f"'working-directory' does not exist: {self.inputs['working_directory']}"
)
elif not path.is_dir():
validation_errors.append(
f"'working-directory' is not a directory: {self.inputs['working_directory']}"
)
# Validate boolean flags
boolean_flags = {"cache_cross_binary", "strip", "use_rust_cache"}
for flag in boolean_flags:
if flag in self.inputs and self.inputs[flag] not in {"true", "false"}:
validation_errors.append(f"'{flag}' must be either 'true' or 'false'")
# Validate rust-cache-parameters JSON if present
if "rust_cache_parameters" in self.inputs:
try:
json.loads(self.inputs["rust_cache_parameters"])
except json.JSONDecodeError:
validation_errors.append("'rust-cache-parameters' must be valid JSON")
return validation_errors
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 TestInputValidator(unittest.TestCase):
"""Unit tests for the InputValidator."""
def setUp(self) -> None:
"""Set up test environment."""
# Clear existing INPUTS_ environment variables
for key in list(os.environ.keys()):
if key.startswith("INPUTS_"):
del os.environ[key]
def setup_env(self, inputs: Dict[str, str]) -> None:
"""Helper function to 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 = {
"target": "x86_64-unknown-linux-gnu",
"command": "build",
"toolchain": "stable",
"use-rust-cache": "true",
}
self.setup_env(inputs)
validator = InputValidator("/root")
for key, value in validator.inputs.items():
self.assertEqual(value, inputs[key.replace("_", "-")])
def test_validate_missing_target(self) -> None:
"""Test validation with missing target."""
self.setup_env({})
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors)
def test_validate_valid_command(self) -> None:
"""Test validation of valid commands."""
valid_commands = ["build", "test", "both", "bench"]
for command in valid_commands:
self.setup_env({"target": "x86_64-unknown-linux-gnu", "command": command})
validator = InputValidator("/root")
errors = validator.validate()
self.assertFalse(errors, f"Command '{command}' should be valid")
def test_validate_invalid_command(self) -> None:
"""Test validation of invalid command."""
self.setup_env({"target": "x86_64-unknown-linux-gnu", "command": "invalid"})
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors)
def test_validate_valid_toolchain(self) -> None:
"""Test validation of valid toolchains."""
valid_toolchains = ["stable", "beta", "nightly"]
for toolchain in valid_toolchains:
self.setup_env(
{"target": "x86_64-unknown-linux-gnu", "toolchain": toolchain}
)
validator = InputValidator("/root")
errors = validator.validate()
self.assertFalse(errors, f"Toolchain '{toolchain}' should be valid")
def test_validate_invalid_toolchain(self) -> None:
"""Test validation of invalid toolchain."""
self.setup_env({"target": "x86_64-unknown-linux-gnu", "toolchain": "unknown"})
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors)
def test_validate_working_directory(self) -> None:
"""Test validation of working directory."""
with tempfile.TemporaryDirectory() as temp_dir:
# Test with valid directory
self.setup_env(
{"target": "x86_64-unknown-linux-gnu", "working-directory": temp_dir}
)
validator = InputValidator("/root")
errors = validator.validate()
self.assertFalse(errors)
# Test with non-existent directory
self.setup_env(
{
"target": "x86_64-unknown-linux-gnu",
"working-directory": "/path/to/nonexistent/directory",
}
)
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors)
# Test with file instead of directory
with tempfile.NamedTemporaryFile() as temp_file:
self.setup_env(
{
"target": "x86_64-unknown-linux-gnu",
"working-directory": temp_file.name,
}
)
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors)
def test_validate_boolean_flags(self) -> None:
"""Test validation of boolean flags."""
boolean_flags = ["cache-cross-binary", "strip", "use-rust-cache"]
# Test valid boolean values
for flag in boolean_flags:
for value in ["true", "false"]:
self.setup_env({"target": "x86_64-unknown-linux-gnu", flag: value})
validator = InputValidator("/root")
errors = validator.validate()
self.assertFalse(errors, f"'{flag}' with '{value}' should be valid")
# Test invalid boolean values
for flag in boolean_flags:
self.setup_env({"target": "x86_64-unknown-linux-gnu", flag: "invalid"})
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors, f"'{flag}' with 'invalid' should be invalid")
def test_validate_rust_cache_parameters(self) -> None:
"""Test validation of rust cache parameters."""
# Valid JSON
self.setup_env(
{
"target": "x86_64-unknown-linux-gnu",
"rust-cache-parameters": '{"key1":"value1","key2":"value2"}',
}
)
validator = InputValidator("/root")
errors = validator.validate()
self.assertFalse(errors)
# Invalid JSON
self.setup_env(
{
"target": "x86_64-unknown-linux-gnu",
"rust-cache-parameters": "{invalid json",
}
)
validator = InputValidator("/root")
errors = validator.validate()
self.assertTrue(errors)
if __name__ == "__main__":
if len(os.sys.argv) > 1 and os.sys.argv[1] == "--test":
unittest.main(argv=["unittest"])
else:
main()