Files
actions/eslint-check/action.yml
2025-11-03 12:59:49 +02:00

439 lines
14 KiB
YAML

# 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: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42
- name: Cache Node Dependencies
id: cache
uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42
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" <<EOF
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "import"],
"env": {
"es2022": true,
"node": true
}
}
EOF
fi
# Create default ignore file if none exists
if [ ! -f "$IGNORE_FILE" ]; then
echo "Creating default ESLint ignore file..."
cat > "$IGNORE_FILE" <<EOF
node_modules/
dist/
build/
coverage/
*.min.js
EOF
fi
- name: Run ESLint Check
id: lint
shell: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
FILE_EXTENSIONS: ${{ inputs.file-extensions }}
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
CONFIG_FILE: ${{ inputs.config-file }}
IGNORE_FILE: ${{ inputs.ignore-file }}
CACHE: ${{ inputs.cache }}
MAX_WARNINGS: ${{ inputs.max-warnings }}
REPORT_FORMAT: ${{ inputs.report-format }}
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
run: |
set -euo pipefail
cd "$WORKING_DIRECTORY"
# Create reports directory
mkdir -p reports
# Prepare file extensions for ESLint
IFS=',' read -ra EXTENSIONS <<< "$FILE_EXTENSIONS"
ext_pattern=""
for ext in "${EXTENSIONS[@]}"; do
ext_pattern="$ext_pattern --ext $ext"
done
# Run ESLint
echo "Running ESLint with $PACKAGE_MANAGER..."
# Build ESLint command based on package manager
case "$PACKAGE_MANAGER" in
"pnpm")
eslint_cmd="pnpm exec eslint"
;;
"yarn")
eslint_cmd="yarn eslint"
;;
"bun")
eslint_cmd="bunx eslint"
;;
"npm"|*)
eslint_cmd="npx eslint"
;;
esac
# Prepare cache flag
cache_flag=""
if [ "$CACHE" = "true" ]; then
cache_flag="--cache"
fi
# Execute ESLint with all arguments
$eslint_cmd $ext_pattern \
--config "$CONFIG_FILE" \
--ignore-path "$IGNORE_FILE" \
${cache_flag} \
--max-warnings "$MAX_WARNINGS" \
--format="$REPORT_FORMAT" \
--output-file="reports/eslint.$REPORT_FORMAT" \
. || {
error_code=$?
# Count errors and warnings
if [ "$REPORT_FORMAT" = "json" ]; then
error_count=$(jq '[.[] | .errorCount] | add' reports/eslint.json)
warning_count=$(jq '[.[] | .warningCount] | add' reports/eslint.json)
else
error_count=$(grep -c '"level": "error"' reports/eslint.sarif || echo 0)
warning_count=$(grep -c '"level": "warning"' reports/eslint.sarif || echo 0)
fi
echo "error_count=${error_count}" >> $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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
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/