CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/jenkinsfile-validator

Comprehensive toolkit for validating, linting, testing, and automating Jenkinsfile pipelines (both Declarative and Scripted). Use this skill when working with Jenkins pipeline files, validating pipeline syntax, checking best practices, debugging pipeline issues, or working with custom plugins.

Overall
score

93%

Does it follow best practices?

Validation for skill structure

Overview
Skills
Evals
Files

validate_shared_library.shscripts/

#!/bin/bash

# Shared Library Validator
# Validates Jenkins Shared Library files (vars/*.groovy, src/**/*.groovy)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'

# Counters
ERRORS=0
WARNINGS=0
INFO=0

# Validation result arrays
declare -a ERROR_MESSAGES=()
declare -a WARNING_MESSAGES=()
declare -a INFO_MESSAGES=()

usage() {
    echo -e "${BOLD}Usage:${NC} $0 <file_or_directory>"
    echo ""
    echo "Validates Jenkins Shared Library Groovy files."
    echo ""
    echo -e "${BOLD}Examples:${NC}"
    echo "  $0 vars/myStep.groovy      # Validate single global variable file"
    echo "  $0 vars/                   # Validate all vars/*.groovy files"
    echo "  $0 src/                    # Validate src/**/*.groovy files"
    echo "  $0 .                       # Validate entire shared library"
    echo ""
    echo -e "${BOLD}File Types:${NC}"
    echo "  vars/*.groovy    - Global variable definitions (callable steps)"
    echo "  src/**/*.groovy  - Groovy source classes"
    echo ""
    exit 1
}

log_error() {
    local line=$1
    local message=$2
    ERROR_MESSAGES+=("ERROR [Line $line]: $message")
    ERRORS=$((ERRORS + 1))
}

log_warning() {
    local line=$1
    local message=$2
    WARNING_MESSAGES+=("WARNING [Line $line]: $message")
    WARNINGS=$((WARNINGS + 1))
}

log_info() {
    local line=$1
    local message=$2
    INFO_MESSAGES+=("INFO [Line $line]: $message")
    INFO=$((INFO + 1))
}

# Detect file type within shared library
detect_library_file_type() {
    local file=$1
    local filepath=$(realpath "$file" 2>/dev/null || echo "$file")

    if [[ "$filepath" == */vars/*.groovy ]]; then
        echo "vars"
    elif [[ "$filepath" == */src/*.groovy ]] || [[ "$filepath" == */src/**/*.groovy ]]; then
        echo "src"
    elif [[ "$file" == *.groovy ]]; then
        # Check content to determine type
        if grep -qE '^\s*def\s+call\s*\(' "$file" 2>/dev/null; then
            echo "vars"
        elif grep -qE '^\s*(class|interface|enum|trait)\s+' "$file" 2>/dev/null; then
            echo "src"
        else
            echo "unknown"
        fi
    else
        echo "unknown"
    fi
}

# Validate vars/*.groovy global variable files
validate_vars_file() {
    local file=$1
    local filename=$(basename "$file" .groovy)
    local line_num=0
    local has_call_method=false
    local has_doc_comment=false
    local in_call_method=false
    local brace_count=0

    echo -e "${BLUE}=== Validating Global Variable: ${BOLD}$filename${NC}${BLUE} ===${NC}"
    echo "File: $file"
    echo ""

    # Check for call() method
    if grep -qE '^\s*def\s+call\s*\(' "$file"; then
        has_call_method=true
    fi

    # Check for documentation comment
    if grep -qE '^\s*/\*\*' "$file"; then
        has_doc_comment=true
    fi

    # Line by line validation
    while IFS= read -r line || [[ -n "$line" ]]; do
        line_num=$((line_num + 1))

        # Skip empty lines and comments for some checks
        if [[ -z "${line// }" ]] || [[ "$line" =~ ^[[:space:]]*//.* ]] || [[ "$line" =~ ^[[:space:]]*\*.* ]]; then
            continue
        fi

        # Check for hardcoded credentials
        if echo "$line" | grep -qiE '(password|secret|token|api[_-]?key)\s*=\s*["\047][a-zA-Z0-9]'; then
            log_error "$line_num" "Potential hardcoded credential detected"
            log_error "$line_num" "  → Use credentials() or parameters instead"
        fi

        # Check for @NonCPS annotation usage
        if echo "$line" | grep -q '@NonCPS'; then
            # Check next non-empty line for pipeline steps
            local next_lines=$(tail -n +$((line_num + 1)) "$file" | head -20)
            if echo "$next_lines" | grep -qE '^\s*(sh|bat|echo|checkout|archiveArtifacts|junit|input|build)\s'; then
                log_error "$line_num" "@NonCPS method contains pipeline steps (sh, echo, etc.)"
                log_error "$line_num" "  → Pipeline steps cannot be used in @NonCPS methods"
                log_error "$line_num" "  → Move pipeline steps outside the @NonCPS method"
            fi
        fi

        # Check for proper error handling in pipeline steps
        if echo "$line" | grep -qE '^\s*sh\s+["\047]'; then
            # Check if inside try block
            local context=$(head -n $line_num "$file" | tail -20)
            if ! echo "$context" | grep -q 'try\s*{'; then
                log_info "$line_num" "Consider wrapping sh step in try-catch for error handling"
            fi
        fi

        # Check for return type documentation
        if echo "$line" | grep -qE '^\s*def\s+call\s*\(' && ! echo "$line" | grep -qE '^\s*(String|boolean|int|def|void|Map|List)'; then
            log_info "$line_num" "Consider specifying return type for call() method"
        fi

        # Check for deprecated methods
        if echo "$line" | grep -qE 'new\s+File\s*\('; then
            log_warning "$line_num" "Using 'new File()' - prefer readFile/writeFile for pipeline compatibility"
            log_warning "$line_num" "  → Use readFile('path') instead of new File('path').text"
        fi

        # Check for URL/HTTP on controller
        if echo "$line" | grep -qE 'new\s+URL\s*\(' || echo "$line" | grep -q 'HttpURLConnection'; then
            log_warning "$line_num" "HTTP operations run on controller - use httpRequest step or curl on agent"
            log_warning "$line_num" "  → Use: sh 'curl ...' or httpRequest plugin"
        fi

        # Check for JsonSlurper on controller
        if echo "$line" | grep -q 'JsonSlurper'; then
            log_warning "$line_num" "JsonSlurper runs on controller - consider using jq on agent for large files"
            log_warning "$line_num" "  → For small JSON: JsonSlurperClassic is safer"
            log_warning "$line_num" "  → For large JSON: sh 'jq ... file.json'"
        fi

        # Check for proper CPS handling with closures
        if echo "$line" | grep -qE '\.(each|collect|find|findAll|any|every)\s*\{'; then
            # Check if marked @NonCPS
            local prev_lines=$(head -n $line_num "$file" | tail -10)
            if ! echo "$prev_lines" | grep -q '@NonCPS'; then
                log_info "$line_num" "Groovy closure detected - ensure method is @NonCPS if not using pipeline steps"
                log_info "$line_num" "  → Closures like .each{}, .collect{} require @NonCPS annotation"
            fi
        fi

        # Check for environment variable access
        if echo "$line" | grep -qE 'System\.getenv\s*\('; then
            log_warning "$line_num" "Using System.getenv() - prefer env.VAR_NAME for pipeline compatibility"
        fi

        # Check for sleep usage
        if echo "$line" | grep -qE 'Thread\.sleep\s*\('; then
            log_warning "$line_num" "Using Thread.sleep() - prefer sleep() step for pipeline compatibility"
        fi

    done < "$file"

    # Post-file checks
    if [ "$has_call_method" = false ]; then
        log_warning "1" "No call() method found - file may not be callable as a step"
        log_warning "1" "  → Add: def call(Map config = [:]) { ... }"
    fi

    if [ "$has_doc_comment" = false ]; then
        log_info "1" "No documentation comment found - consider adding /** ... */ block"
        log_info "1" "  → Documents step usage for Pipeline Syntax generator"
    fi

    # Check file naming convention
    if [[ ! "$filename" =~ ^[a-z][a-zA-Z0-9]*$ ]]; then
        log_warning "1" "Filename '$filename' should be camelCase starting with lowercase"
        log_warning "1" "  → Convention: myStepName.groovy"
    fi
}

# Validate src/**/*.groovy class files
validate_src_file() {
    local file=$1
    local filename=$(basename "$file" .groovy)
    local line_num=0
    local has_package=false
    local has_class=false
    local has_serializable=false

    echo -e "${BLUE}=== Validating Source Class: ${BOLD}$filename${NC}${BLUE} ===${NC}"
    echo "File: $file"
    echo ""

    # Check for package declaration
    if grep -qE '^\s*package\s+' "$file"; then
        has_package=true
    fi

    # Check for class/interface/enum
    if grep -qE '^\s*(class|interface|enum|trait)\s+' "$file"; then
        has_class=true
    fi

    # Check for Serializable
    if grep -q 'implements.*Serializable' "$file"; then
        has_serializable=true
    fi

    while IFS= read -r line || [[ -n "$line" ]]; do
        line_num=$((line_num + 1))

        # Skip empty lines and comments
        if [[ -z "${line// }" ]] || [[ "$line" =~ ^[[:space:]]*//.* ]]; then
            continue
        fi

        # Check for hardcoded credentials
        if echo "$line" | grep -qiE '(password|secret|token|api[_-]?key)\s*=\s*["\047][a-zA-Z0-9]'; then
            log_error "$line_num" "Potential hardcoded credential detected"
        fi

        # Check for proper import usage
        if echo "$line" | grep -qE '^\s*import\s+' && echo "$line" | grep -q '\*'; then
            log_info "$line_num" "Wildcard import detected - prefer explicit imports"
        fi

        # Check for static method usage in CPS context
        if echo "$line" | grep -qE 'static.*def\s+' && ! grep -q '@NonCPS' "$file"; then
            log_info "$line_num" "Static method detected - ensure CPS compatibility or add @NonCPS"
        fi

    done < "$file"

    # Post-file checks
    if [ "$has_class" = true ] && [ "$has_serializable" = false ]; then
        log_warning "1" "Class does not implement Serializable - may cause CPS issues"
        log_warning "1" "  → Add: implements Serializable"
    fi

    if [ "$has_class" = true ] && [ "$has_package" = false ]; then
        log_warning "1" "No package declaration - add package statement"
        log_warning "1" "  → Add: package com.example.jenkins"
    fi

    # Check class naming matches filename
    local class_name=$(grep -oE '^\s*(class|interface|enum|trait)\s+([A-Z][a-zA-Z0-9]*)' "$file" | head -1 | awk '{print $2}')
    if [ -n "$class_name" ] && [ "$class_name" != "$filename" ]; then
        log_error "1" "Class name '$class_name' does not match filename '$filename'"
    fi
}

# Print validation results
print_results() {
    echo ""
    echo -e "${BLUE}=== Validation Results ===${NC}"
    echo ""

    if [ ${#ERROR_MESSAGES[@]} -gt 0 ]; then
        echo -e "${RED}ERRORS (${ERRORS}):${NC}"
        for msg in "${ERROR_MESSAGES[@]}"; do
            echo -e "${RED}$msg${NC}"
        done
        echo ""
    fi

    if [ ${#WARNING_MESSAGES[@]} -gt 0 ]; then
        echo -e "${YELLOW}WARNINGS (${WARNINGS}):${NC}"
        for msg in "${WARNING_MESSAGES[@]}"; do
            echo -e "${YELLOW}$msg${NC}"
        done
        echo ""
    fi

    if [ ${#INFO_MESSAGES[@]} -gt 0 ]; then
        echo -e "${BLUE}INFO (${INFO}):${NC}"
        for msg in "${INFO_MESSAGES[@]}"; do
            echo -e "${BLUE}$msg${NC}"
        done
        echo ""
    fi

    echo -e "${BLUE}=== Summary ===${NC}"
    if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
        echo -e "${GREEN}✓ Validation passed with no errors or warnings${NC}"
    elif [ $ERRORS -eq 0 ]; then
        echo -e "${YELLOW}⚠ Validation passed with ${WARNINGS} warning(s)${NC}"
    else
        echo -e "${RED}✗ Validation failed with ${ERRORS} error(s) and ${WARNINGS} warning(s)${NC}"
    fi
}

# Validate a single file
validate_file() {
    local file=$1
    local file_type=$(detect_library_file_type "$file")

    case "$file_type" in
        vars)
            validate_vars_file "$file"
            ;;
        src)
            validate_src_file "$file"
            ;;
        unknown)
            echo -e "${YELLOW}Warning: Could not determine file type for $file${NC}"
            echo "Treating as vars file..."
            validate_vars_file "$file"
            ;;
    esac
}

# Validate directory of files
validate_directory() {
    local dir=$1
    local file_count=0

    echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
    echo -e "${CYAN}  ${BOLD}Jenkins Shared Library Validator${NC}"
    echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
    echo ""
    echo -e "Directory: ${BOLD}$dir${NC}"
    echo ""

    # Find and validate vars files
    if [ -d "$dir/vars" ]; then
        echo -e "${BLUE}--- Validating vars/ directory ---${NC}"
        for file in "$dir/vars"/*.groovy; do
            if [ -f "$file" ]; then
                file_count=$((file_count + 1))
                validate_file "$file"
                echo ""
            fi
        done
    fi

    # Find and validate src files
    if [ -d "$dir/src" ]; then
        echo -e "${BLUE}--- Validating src/ directory ---${NC}"
        while IFS= read -r file; do
            if [ -f "$file" ]; then
                file_count=$((file_count + 1))
                validate_file "$file"
                echo ""
            fi
        done < <(find "$dir/src" -name "*.groovy" -type f 2>/dev/null)
    fi

    # If no vars or src, validate all groovy files
    if [ $file_count -eq 0 ]; then
        echo -e "${YELLOW}No vars/ or src/ directory found. Validating all .groovy files...${NC}"
        while IFS= read -r file; do
            if [ -f "$file" ]; then
                file_count=$((file_count + 1))
                validate_file "$file"
                echo ""
            fi
        done < <(find "$dir" -name "*.groovy" -type f 2>/dev/null)
    fi

    if [ $file_count -eq 0 ]; then
        echo -e "${YELLOW}No Groovy files found in $dir${NC}"
        exit 0
    fi

    echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
    echo -e "  ${BOLD}Validated ${file_count} file(s)${NC}"
    echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
}

# Main execution
if [ $# -ne 1 ]; then
    usage
fi

TARGET="$1"

if [ -f "$TARGET" ]; then
    echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
    echo -e "${CYAN}  ${BOLD}Jenkins Shared Library Validator${NC}"
    echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════════${NC}"
    echo ""
    validate_file "$TARGET"
    print_results
elif [ -d "$TARGET" ]; then
    validate_directory "$TARGET"
    print_results
else
    echo -e "${RED}Error: '$TARGET' is not a valid file or directory${NC}"
    exit 2
fi

# Exit with appropriate code
if [ $ERRORS -gt 0 ]; then
    exit 1
fi
exit 0

Install with Tessl CLI

npx tessl i pantheon-ai/jenkinsfile-validator@0.1.0

SKILL.md

tile.json