#!/bin/bash set -e # CI Template Validation Script # This script validates the syntax and structure of Gitea Actions workflow templates # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration WORKFLOW_DIR=".gitea/workflows" REQUIRED_FIELDS=("name" "on" "jobs") REUSABLE_TRIGGER="workflow_call" # Counters TOTAL_FILES=0 VALID_FILES=0 WARNINGS=0 # Helper functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" ((WARNINGS++)) } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Check if required tools are installed check_dependencies() { log_info "Checking dependencies..." if ! command -v yamllint &> /dev/null; then log_warning "yamllint not found. Install with: pip install yamllint" log_info "Will skip YAML syntax validation" SKIP_YAMLLINT=true else log_success "yamllint found" fi if ! command -v yq &> /dev/null; then log_warning "yq not found. Install with: brew install yq (macOS) or apt install yq (Ubuntu)" log_info "Will use basic grep validation instead" SKIP_YQ=true else log_success "yq found" fi } # Validate YAML syntax validate_yaml_syntax() { local file="$1" if [[ "$SKIP_YAMLLINT" == "true" ]]; then return 0 fi if yamllint "$file" 2>/dev/null; then return 0 else log_error "YAML syntax validation failed for $file" return 1 fi } # Check if file is a reusable workflow is_reusable_workflow() { local file="$1" if [[ "$SKIP_YQ" == "true" ]]; then # Fallback to grep if grep -q "workflow_call" "$file"; then return 0 fi else # Use yq for precise checking if yq eval '.on | has("workflow_call")' "$file" 2>/dev/null | grep -q "true"; then return 0 fi fi return 1 } # Validate required workflow fields validate_workflow_structure() { local file="$1" local valid=true for field in "${REQUIRED_FIELDS[@]}"; do if [[ "$SKIP_YQ" == "true" ]]; then # Fallback to grep if ! grep -q "^${field}:" "$file"; then log_error "Missing required field '$field' in $file" valid=false fi else # Use yq for precise checking if ! yq eval "has(\"$field\")" "$file" 2>/dev/null | grep -q "true"; then log_error "Missing required field '$field' in $file" valid=false fi fi done if [[ "$valid" == "true" ]]; then return 0 else return 1 fi } # Validate workflow inputs (for reusable workflows) validate_workflow_inputs() { local file="$1" if ! is_reusable_workflow "$file"; then return 0 fi if [[ "$SKIP_YQ" == "true" ]]; then # Basic check with grep if grep -q "inputs:" "$file"; then log_success "Inputs section found in $file" else log_warning "No inputs section found in reusable workflow $file" fi else # Detailed validation with yq local inputs_count inputs_count=$(yq eval '.on.workflow_call.inputs | length' "$file" 2>/dev/null || echo "0") if [[ "$inputs_count" -gt 0 ]]; then log_success "Found $inputs_count input(s) in $file" # Validate each input has required fields local input_names input_names=$(yq eval '.on.workflow_call.inputs | keys | .[]' "$file" 2>/dev/null || echo "") while IFS= read -r input_name; do if [[ -n "$input_name" ]]; then # Check for description if ! yq eval ".on.workflow_call.inputs.${input_name} | has(\"description\")" "$file" 2>/dev/null | grep -q "true"; then log_warning "Input '$input_name' missing description in $file" fi # Check for type if ! yq eval ".on.workflow_call.inputs.${input_name} | has(\"type\")" "$file" 2>/dev/null | grep -q "true"; then log_warning "Input '$input_name' missing type in $file" fi fi done <<< "$input_names" else log_warning "No inputs found in reusable workflow $file" fi fi } # Validate a single workflow file validate_workflow_file() { local file="$1" local filename filename=$(basename "$file") log_info "Validating $file..." ((TOTAL_FILES++)) # Check if file exists and is readable if [[ ! -r "$file" ]]; then log_error "Cannot read file: $file" return 1 fi # Validate YAML syntax if ! validate_yaml_syntax "$file"; then return 1 fi # Validate workflow structure if ! validate_workflow_structure "$file"; then return 1 fi # Additional validations for reusable workflows if is_reusable_workflow "$file"; then log_success "$filename is a reusable workflow" validate_workflow_inputs "$file" else # Skip warning for test files if [[ "$filename" != *"test-"* ]]; then log_warning "$filename is not a reusable workflow (missing workflow_call trigger)" fi fi # Check for common issues if grep -q "uses: \./\|uses: \.\\\\" "$file"; then # Skip warning for test files - they intentionally use relative paths to test local workflows if [[ "$filename" != *"test-"* ]]; then log_warning "Found relative path in 'uses' field in $file - this may cause issues" fi fi # Check for hardcoded values that should be inputs if grep -q "python-version: [\"']3\." "$file"; then # Skip warning for test files if [[ "$filename" != *"test-"* ]]; then log_warning "Found hardcoded Python version in $file - consider making it an input" fi fi ((VALID_FILES++)) log_success "$filename validation completed" return 0 } # Validate all workflow files validate_all_workflows() { log_info "Starting workflow validation..." if [[ ! -d "$WORKFLOW_DIR" ]]; then log_error "Workflow directory '$WORKFLOW_DIR' not found" exit 1 fi # Find all YAML files in workflow directory local workflow_files workflow_files=$(find "$WORKFLOW_DIR" -name "*.yml" -o -name "*.yaml" 2>/dev/null) if [[ -z "$workflow_files" ]]; then log_error "No workflow files found in '$WORKFLOW_DIR'" exit 1 fi # Validate each file local failed_files=0 while IFS= read -r file; do if [[ -n "$file" ]]; then if ! validate_workflow_file "$file"; then ((failed_files++)) fi fi done <<< "$workflow_files" # Print summary echo log_info "=== Validation Summary ===" log_info "Total files: $TOTAL_FILES" log_success "Valid files: $VALID_FILES" if [[ $WARNINGS -gt 0 ]]; then log_warning "Warnings: $WARNINGS" fi if [[ $failed_files -gt 0 ]]; then log_error "Failed files: $failed_files" exit 1 fi if [[ $WARNINGS -gt 0 ]]; then log_warning "Validation completed with warnings" exit 0 fi log_success "All workflows validated successfully!" } # Validate documentation files validate_documentation() { log_info "Validating documentation..." local readme_files=("README.md" "README-python-code-quality.md" "README-python-pytest.md" "README-web-formatting.md") for readme in "${readme_files[@]}"; do if [[ -f "$readme" ]]; then log_success "Found $readme" # Check for basic sections if ! grep -q "^# " "$readme"; then log_warning "$readme missing main heading" fi if ! grep -q "## Usage" "$readme"; then log_warning "$readme missing Usage section" fi else log_warning "Missing documentation file: $readme" fi done } # Generate workflow file list list_workflows() { log_info "Available workflow templates:" echo if [[ -d "$WORKFLOW_DIR" ]]; then find "$WORKFLOW_DIR" -name "*.yml" -o -name "*.yaml" | while read -r file; do local relative_path relative_path=$(echo "$file" | sed "s|^$WORKFLOW_DIR/||") if is_reusable_workflow "$file"; then echo -e " ${GREEN}✓${NC} $relative_path (reusable)" else echo -e " ${YELLOW}○${NC} $relative_path (not reusable)" fi done else log_error "Workflow directory not found" fi echo } # Main function main() { echo -e "${BLUE}CI Template Validator${NC}" echo "======================" echo # Parse command line arguments case "${1:-validate}" in "validate") check_dependencies validate_all_workflows validate_documentation ;; "list") list_workflows ;; "help"|"-h"|"--help") echo "Usage: $0 [command]" echo echo "Commands:" echo " validate Validate all workflow files (default)" echo " list List all workflow templates" echo " help Show this help message" echo echo "Examples:" echo " $0 # Validate all workflows" echo " $0 validate # Same as above" echo " $0 list # List all available templates" ;; *) log_error "Unknown command: $1" echo "Use '$0 help' for usage information" exit 1 ;; esac } # Run main function with all arguments main "$@" echo -e "${BLUE}🔍 CI Template Validation Script${NC}" echo "==================================" # Function to print status print_status() { if [ $1 -eq 0 ]; then echo -e "${GREEN}✅ $2${NC}" else echo -e "${RED}❌ $2${NC}" return 1 fi } # Function to print warning print_warning() { echo -e "${YELLOW}âš ī¸ $1${NC}" } # Function to print info print_info() { echo -e "${BLUE}â„šī¸ $1${NC}" } # Check if required tools are installed check_dependencies() { echo -e "\n${BLUE}Checking dependencies...${NC}" local missing_deps=() if ! command -v yamllint &> /dev/null; then missing_deps+=("yamllint") fi if ! command -v python3 &> /dev/null; then missing_deps+=("python3") fi if [ ${#missing_deps[@]} -gt 0 ]; then echo -e "${RED}Missing dependencies: ${missing_deps[*]}${NC}" echo "Install with:" for dep in "${missing_deps[@]}"; do case $dep in yamllint) echo " pip install yamllint" ;; python3) echo " Install Python 3.x" ;; esac done return 1 fi print_status 0 "All dependencies available" } # Validate YAML syntax validate_yaml_syntax() { echo -e "\n${BLUE}Validating YAML syntax...${NC}" local yaml_files=() while IFS= read -r -d $'\0'; do yaml_files+=("$REPLY") done < <(find .gitea/workflows -name "*.yml" -print0 2>/dev/null) if [ ${#yaml_files[@]} -eq 0 ]; then print_warning "No YAML files found in .gitea/workflows/" return 1 fi local errors=0 for file in "${yaml_files[@]}"; do if yamllint "$file" > /dev/null 2>&1; then print_status 0 "YAML syntax valid: $file" else print_status 1 "YAML syntax error: $file" yamllint "$file" ((errors++)) fi done if [ $errors -eq 0 ]; then print_status 0 "All YAML files have valid syntax" else print_status 1 "$errors YAML files have syntax errors" return 1 fi } # Validate workflow structure validate_workflow_structure() { echo -e "\n${BLUE}Validating workflow structure...${NC}" local workflow_files=() while IFS= read -r -d $'\0'; do workflow_files+=("$REPLY") done < <(find .gitea/workflows -name "*.yml" -print0 2>/dev/null) local errors=0 for file in "${workflow_files[@]}"; do echo -e "\nChecking $file..." # Check for workflow_call trigger if grep -q "workflow_call" "$file"; then print_status 0 "Has workflow_call trigger" else print_status 1 "Missing workflow_call trigger" ((errors++)) fi # Check for inputs section if grep -q "inputs:" "$file"; then print_status 0 "Has inputs section" else print_warning "No inputs section (may be intentional)" fi # Check for jobs section if grep -q "jobs:" "$file"; then print_status 0 "Has jobs section" else print_status 1 "Missing jobs section" ((errors++)) fi # Check for runs-on if grep -q "runs-on:" "$file"; then print_status 0 "Has runs-on specified" else print_status 1 "Missing runs-on specification" ((errors++)) fi # Check for checkout action if grep -q "actions/checkout" "$file"; then print_status 0 "Uses checkout action" else print_warning "No checkout action (may be intentional)" fi done if [ $errors -eq 0 ]; then print_status 0 "All workflows have valid structure" else print_status 1 "$errors workflows have structure issues" return 1 fi } # Validate input parameters validate_input_parameters() { echo -e "\n${BLUE}Validating input parameters...${NC}" local workflow_files=() while IFS= read -r -d $'\0'; do workflow_files+=("$REPLY") done < <(find .gitea/workflows -name "*.yml" -print0 2>/dev/null) for file in "${workflow_files[@]}"; do echo -e "\nChecking inputs in $file..." # Extract input definitions and usage local input_definitions=() while IFS= read -r line; do if [[ $line =~ ^[[:space:]]*([a-zA-Z0-9_-]+):[[:space:]]*$ ]]; then input_name="${BASH_REMATCH[1]}" if [[ ! $input_name =~ ^(description|required|type|default)$ ]]; then input_definitions+=("$input_name") fi fi done < <(sed -n '/inputs:/,/^[[:space:]]*[a-zA-Z]/p' "$file" | head -n -1) # Check if inputs are used in the workflow for input in "${input_definitions[@]}"; do if grep -q "\${{ inputs\.$input }}" "$file"; then print_status 0 "Input '$input' is used in workflow" else print_warning "Input '$input' is defined but not used" fi done done } # Create test project structure create_test_project() { echo -e "\n${BLUE}Creating test project structure...${NC}" local test_dir="test-validation-project" if [ -d "$test_dir" ]; then print_info "Removing existing test project" rm -rf "$test_dir" fi mkdir -p "$test_dir/src/test_package" cd "$test_dir" # Create pyproject.toml cat > pyproject.toml << 'EOF' [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "test-validation-package" version = "0.1.0" description = "Test package for CI template validation" dependencies = [] [project.optional-dependencies] dev = [ "mypy>=1.0.0", "ruff>=0.1.0", "pyupgrade>=3.0.0", "pytest>=7.0.0", "pytest-cov>=4.0.0", ] [tool.ruff] line-length = 88 target-version = "py310" [tool.mypy] python_version = "3.10" warn_return_any = true warn_unused_configs = true EOF # Create source code mkdir -p src/test_package cat > src/test_package/__init__.py << 'EOF' """Test package for validation.""" __version__ = "0.1.0" def hello_world() -> str: """Return a greeting message.""" return "Hello, World!" EOF cat > src/test_package/math_utils.py << 'EOF' """Math utility functions.""" from typing import List def add_numbers(numbers: List[float]) -> float: """Add a list of numbers.""" return sum(numbers) def multiply_numbers(numbers: List[float]) -> float: """Multiply a list of numbers.""" result = 1.0 for num in numbers: result *= num return result EOF # Create tests mkdir -p tests cat > tests/test_math_utils.py << 'EOF' """Tests for math utilities.""" import pytest from test_package.math_utils import add_numbers, multiply_numbers def test_add_numbers(): assert add_numbers([1, 2, 3]) == 6 assert add_numbers([]) == 0 assert add_numbers([5.5, 2.5]) == 8.0 def test_multiply_numbers(): assert multiply_numbers([2, 3, 4]) == 24 assert multiply_numbers([1]) == 1 assert multiply_numbers([2.5, 2]) == 5.0 EOF cd .. print_status 0 "Test project created at $test_dir" } # Generate validation report generate_report() { echo -e "\n${BLUE}Generating validation report...${NC}" local report_file="validation-report.md" cat > "$report_file" << 'EOF' # CI Template Validation Report Generated on: $(date) ## Summary This report contains the validation results for the CI template workflows. ## Workflow Files EOF # Add workflow file information find .gitea/workflows -name "*.yml" 2>/dev/null | while read -r file; do echo "### $file" >> "$report_file" echo "" >> "$report_file" echo "- **Size:** $(wc -c < "$file") bytes" >> "$report_file" echo "- **Lines:** $(wc -l < "$file") lines" >> "$report_file" if grep -q "workflow_call" "$file"; then echo "- **Type:** Reusable workflow ✅" >> "$report_file" else echo "- **Type:** Standard workflow âš ī¸" >> "$report_file" fi local input_count=$(grep -c "^[[:space:]]*[a-zA-Z0-9_-]*:[[:space:]]*$" "$file" | head -n 1) echo "- **Input parameters:** $input_count" >> "$report_file" echo "" >> "$report_file" done print_status 0 "Validation report generated: $report_file" } # Main execution main() { echo -e "${BLUE}Starting CI template validation...${NC}" local exit_code=0 # Run all validation steps check_dependencies || exit_code=1 validate_yaml_syntax || exit_code=1 validate_workflow_structure || exit_code=1 validate_input_parameters || exit_code=1 create_test_project || exit_code=1 generate_report || exit_code=1 echo -e "\n${BLUE}Validation Summary${NC}" echo "==================" if [ $exit_code -eq 0 ]; then echo -e "${GREEN}🎉 All validations passed!${NC}" echo -e "${GREEN}Your CI templates are ready for use.${NC}" else echo -e "${RED}đŸ’Ĩ Some validations failed.${NC}" echo -e "${RED}Please review the errors above and fix them.${NC}" fi echo -e "\n${BLUE}Next steps:${NC}" echo "1. Review the validation report" echo "2. Test the templates with the created test project" echo "3. Run the automated test workflow: .gitea/workflows/test-templates.yml" return $exit_code } # Run the main function main "$@"