# yaml-language-server: $schema=https://json.schemastore.org/github-action.json # permissions: # - security-events: write # Required for uploading SARIF results # - contents: read # Required for checking out repository --- name: ESLint Check description: 'Run ESLint check on the repository with advanced configuration and reporting' author: Ismo Vuorinen branding: icon: check-circle color: blue inputs: working-directory: description: 'Directory containing files to lint' required: false default: '.' eslint-version: description: 'ESLint version to use' required: false default: 'latest' config-file: description: 'Path to ESLint config file' required: false default: '.eslintrc' ignore-file: description: 'Path to ESLint ignore file' required: false default: '.eslintignore' file-extensions: description: 'File extensions to lint (comma-separated)' required: false default: '.js,.jsx,.ts,.tsx' cache: description: 'Enable ESLint caching' required: false default: 'true' max-warnings: description: 'Maximum number of warnings allowed' required: false default: '0' fail-on-error: description: 'Fail workflow if issues are found' required: false default: 'true' report-format: description: 'Output format (stylish, json, sarif)' required: false default: 'sarif' max-retries: description: 'Maximum number of retry attempts' required: false default: '3' token: description: 'GitHub token for authentication' required: false default: '' outputs: error-count: description: 'Number of errors found' value: ${{ steps.lint.outputs.error_count }} warning-count: description: 'Number of warnings found' value: ${{ steps.lint.outputs.warning_count }} sarif-file: description: 'Path to SARIF report file' value: ${{ steps.lint.outputs.sarif_file }} files-checked: description: 'Number of files checked' value: ${{ steps.lint.outputs.files_checked }} runs: using: composite steps: - name: Validate Inputs id: validate shell: bash env: WORKING_DIRECTORY: ${{ inputs.working-directory }} ESLINT_VERSION: ${{ inputs.eslint-version }} CONFIG_FILE: ${{ inputs.config-file }} IGNORE_FILE: ${{ inputs.ignore-file }} FILE_EXTENSIONS: ${{ inputs.file-extensions }} CACHE: ${{ inputs.cache }} FAIL_ON_ERROR: ${{ inputs.fail-on-error }} MAX_WARNINGS: ${{ inputs.max-warnings }} REPORT_FORMAT: ${{ inputs.report-format }} MAX_RETRIES: ${{ inputs.max-retries }} run: | set -euo pipefail # Validate working directory exists if [ ! -d "$WORKING_DIRECTORY" ]; then echo "::error::Working directory not found at '$WORKING_DIRECTORY'" exit 1 fi # Validate working directory path security (prevent traversal) if [[ "$WORKING_DIRECTORY" == *".."* ]]; then echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed" exit 1 fi # Validate ESLint version format if [[ -n "$ESLINT_VERSION" ]] && [[ "$ESLINT_VERSION" != "latest" ]]; then if ! [[ "$ESLINT_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?(-[a-zA-Z0-9.-]+)?$ ]]; then echo "::error::Invalid eslint-version format: '$ESLINT_VERSION'. Expected format: X.Y.Z or 'latest' (e.g., 8.57.0, latest)" exit 1 fi fi # Validate config file path if not default if [[ "$CONFIG_FILE" != ".eslintrc" ]] && [[ "$CONFIG_FILE" == *".."* ]]; then echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed" exit 1 fi # Validate ignore file path if not default if [[ "$IGNORE_FILE" != ".eslintignore" ]] && [[ "$IGNORE_FILE" == *".."* ]]; then echo "::error::Invalid ignore file path: '$IGNORE_FILE'. Path traversal not allowed" exit 1 fi # Validate file extensions format if ! [[ "$FILE_EXTENSIONS" =~ ^(\.[a-zA-Z0-9]+)(,\.[a-zA-Z0-9]+)*$ ]]; then echo "::error::Invalid file extensions format: '$FILE_EXTENSIONS'. Expected format: .js,.jsx,.ts,.tsx" exit 1 fi # Validate boolean inputs validate_boolean() { local value="$1" local name="$2" case "${value,,}" in true|false) ;; *) echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false" exit 1 ;; esac } validate_boolean "$CACHE" "cache" validate_boolean "$FAIL_ON_ERROR" "fail-on-error" # Validate max warnings (positive integer) if ! [[ "$MAX_WARNINGS" =~ ^[0-9]+$ ]]; then echo "::error::Invalid max-warnings: '$MAX_WARNINGS'. Must be a non-negative integer (e.g., 0, 10)" exit 1 fi # Validate report format enumerated values case "$REPORT_FORMAT" in stylish|json|sarif|checkstyle|compact|html|jslint-xml|junit|tap|unix) ;; *) echo "::error::Invalid report-format: '$REPORT_FORMAT'. Allowed values: stylish, json, sarif, checkstyle, compact, html, jslint-xml, junit, tap, unix" exit 1 ;; esac # Validate max retries (positive integer with reasonable upper limit) if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10" exit 1 fi - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: token: ${{ inputs.token || github.token }} - name: Setup Node.js id: node-setup uses: ./node-setup - name: Cache Node Dependencies id: cache uses: ./common-cache with: type: 'npm' paths: 'node_modules' key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' key-prefix: 'eslint-check-${{ steps.node-setup.outputs.package-manager }}' - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' shell: bash env: WORKING_DIRECTORY: ${{ inputs.working-directory }} PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} MAX_RETRIES: ${{ inputs.max-retries }} ESLINT_VERSION: ${{ inputs.eslint-version }} run: | set -euo pipefail cd "$WORKING_DIRECTORY" echo "Installing ESLint dependencies using $PACKAGE_MANAGER..." # Function to install with retries install_with_retries() { local attempt=1 while [ $attempt -le "$MAX_RETRIES" ]; do echo "Installation attempt $attempt of $MAX_RETRIES" case "$PACKAGE_MANAGER" in "pnpm") if pnpm add -D \ "eslint@$ESLINT_VERSION" \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ @microsoft/eslint-formatter-sarif \ eslint-plugin-import \ eslint-config-prettier \ typescript; then return 0 fi ;; "yarn") if yarn add -D \ "eslint@$ESLINT_VERSION" \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ @microsoft/eslint-formatter-sarif \ eslint-plugin-import \ eslint-config-prettier \ typescript; then return 0 fi ;; "bun") if bun add -D \ "eslint@$ESLINT_VERSION" \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ @microsoft/eslint-formatter-sarif \ eslint-plugin-import \ eslint-config-prettier \ typescript; then return 0 fi ;; "npm"|*) if npm install \ "eslint@$ESLINT_VERSION" \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ @microsoft/eslint-formatter-sarif \ eslint-plugin-import \ eslint-config-prettier \ typescript; then return 0 fi ;; esac attempt=$((attempt + 1)) if [ $attempt -le "$MAX_RETRIES" ]; then echo "Installation failed, waiting 10 seconds before retry..." sleep 10 fi done echo "::error::Failed to install dependencies after $MAX_RETRIES attempts" return 1 } install_with_retries - name: Prepare ESLint Configuration id: config shell: bash env: WORKING_DIRECTORY: ${{ inputs.working-directory }} CONFIG_FILE: ${{ inputs.config-file }} IGNORE_FILE: ${{ inputs.ignore-file }} run: | set -euo pipefail cd "$WORKING_DIRECTORY" # Create default config if none exists if [ ! -f "$CONFIG_FILE" ]; then echo "Creating default ESLint configuration..." cat > "$CONFIG_FILE" < "$IGNORE_FILE" <> $GITHUB_OUTPUT echo "warning_count=${warning_count}" >> $GITHUB_OUTPUT if [ "$FAIL_ON_ERROR" = "true" ] && [ $error_code -ne 0 ]; then echo "::error::ESLint found ${error_count} errors and ${warning_count} warnings" exit $error_code fi } # Count checked files files_checked=$(find . -type f \( $(printf -- "-name *%s -o " "${EXTENSIONS[@]}") -false \) | wc -l) echo "files_checked=${files_checked}" >> $GITHUB_OUTPUT echo "sarif_file=reports/eslint.sarif" >> $GITHUB_OUTPUT - name: Upload ESLint Results if: always() && inputs.report-format == 'sarif' uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 with: sarif_file: ${{ inputs.working-directory }}/reports/eslint.sarif category: eslint - name: Cache Cleanup if: always() shell: bash env: WORKING_DIRECTORY: ${{ inputs.working-directory }} run: |- set -euo pipefail cd "$WORKING_DIRECTORY" # Clean up ESLint cache if it exists if [ -f ".eslintcache" ]; then rm .eslintcache fi # Remove temporary files rm -rf reports/