mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
439 lines
14 KiB
YAML
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@7061aafd35a2f21b57653e34f2b634b2a19334a9
|
|
|
|
- name: Cache Node Dependencies
|
|
id: cache
|
|
uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9
|
|
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@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/
|