Validate inputs to this action
This commit is contained in:
parent
02640563b4
commit
dad7ec15de
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@ -5,8 +5,8 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
test-action:
|
||||
name: Test action
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -291,3 +291,13 @@ jobs:
|
||||
--expect-cross-version "${{ matrix.platform.expect_cross_version }}" \
|
||||
${{ matrix.platform.expect_cross }} \
|
||||
${{ 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
|
||||
|
||||
@ -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
|
||||
the `target` parameter as part of the cache key automatically. Suggested by @jennydaman (Jennings
|
||||
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
|
||||
|
||||
|
||||
25
action.yml
25
action.yml
@ -6,20 +6,20 @@ branding:
|
||||
description: |
|
||||
Cross compile your Rust projects with cross (https://github.com/cross-rs/cross).
|
||||
inputs:
|
||||
working-directory:
|
||||
description: The working directory for each step
|
||||
default: "."
|
||||
target:
|
||||
description: The target platform
|
||||
required: true
|
||||
command:
|
||||
description: |
|
||||
The commands to run. This must be one of "build", "test", "both" (build and test), or "bench".
|
||||
default: build
|
||||
target:
|
||||
description: The target platform
|
||||
required: true
|
||||
toolchain:
|
||||
description: |
|
||||
The target toolchain to use (one of "stable", "beta", or "nightly").
|
||||
default: stable
|
||||
working-directory:
|
||||
description: The working directory for each step
|
||||
default: "."
|
||||
GITHUB_TOKEN:
|
||||
description: |
|
||||
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
|
||||
shell: bash
|
||||
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
|
||||
id: determine-cross-compile
|
||||
shell: bash
|
||||
|
||||
@ -3,6 +3,24 @@ exclude = [
|
||||
"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]
|
||||
type = "both"
|
||||
include = "**/*"
|
||||
|
||||
261
validate-inputs.py
Executable file
261
validate-inputs.py
Executable 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()
|
||||
Loading…
x
Reference in New Issue
Block a user