Files
actions/eslint-lint/action.yml
Ismo Vuorinen 1b6d7240a8 refactor: migrate Node.js linters from common-cache to actions/cache
Replace common-cache wrapper with native actions/cache@v4.3.0 in all
Node.js linting actions.

Changes:
- biome-lint: Use actions/cache with direct hashFiles()
- eslint-lint: Use actions/cache with direct hashFiles()
- prettier-lint: Use actions/cache with direct hashFiles()
- pr-lint: Use actions/cache with direct hashFiles()

All actions now use:
- Native GitHub Actions cache functionality
- Multi-lock-file support (npm, yarn, pnpm, bun)
- Two-level restore-keys for graceful fallback
- OS-aware cache keys with runner.os

Benefits:
- No wrapper overhead
- Native hashFiles() instead of manual SHA256
- Consistent caching pattern across all Node.js actions
2025-11-20 15:09:58 +02:00

426 lines
14 KiB
YAML

# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
# permissions:
# - contents: write # Required for fix mode to push changes
# - security-events: write # Required for check mode to upload SARIF
---
name: ESLint Lint
description: 'Run ESLint in check or fix mode with advanced configuration and reporting'
author: Ismo Vuorinen
branding:
icon: check-circle
color: blue
inputs:
mode:
description: 'Mode to run (check or fix)'
required: false
default: 'check'
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 (check mode only)'
required: false
default: '0'
fail-on-error:
description: 'Fail workflow if issues are found (check mode only)'
required: false
default: 'true'
report-format:
description: 'Output format for check mode (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: ''
username:
description: 'GitHub username for commits (fix mode only)'
required: false
default: 'github-actions'
email:
description: 'GitHub email for commits (fix mode only)'
required: false
default: 'github-actions@github.com'
outputs:
status:
description: 'Overall status (success/failure)'
value: ${{ steps.check.outputs.status || steps.fix.outputs.status }}
error-count:
description: 'Number of errors found (check mode only)'
value: ${{ steps.check.outputs.error_count }}
warning-count:
description: 'Number of warnings found (check mode only)'
value: ${{ steps.check.outputs.warning_count }}
sarif-file:
description: 'Path to SARIF report file (check mode only)'
value: ${{ steps.check.outputs.sarif_file }}
files-checked:
description: 'Number of files checked (check mode only)'
value: ${{ steps.check.outputs.files_checked }}
files-changed:
description: 'Number of files changed (fix mode only)'
value: ${{ steps.fix.outputs.files_changed }}
errors-fixed:
description: 'Number of errors fixed (fix mode only)'
value: ${{ steps.fix.outputs.errors_fixed }}
runs:
using: composite
steps:
- name: Validate Inputs
id: validate
shell: bash
env:
MODE: ${{ inputs.mode }}
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 }}
EMAIL: ${{ inputs.email }}
USERNAME: ${{ inputs.username }}
run: |
set -euo pipefail
# Validate mode
case "$MODE" in
"check"|"fix")
echo "Mode: $MODE"
;;
*)
echo "::error::Invalid mode: '$MODE'. Must be 'check' or 'fix'"
exit 1
;;
esac
# 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"
exit 1
fi
# Validate report format
case "$REPORT_FORMAT" in
stylish|json|sarif)
;;
*)
echo "::error::Invalid report-format: '$REPORT_FORMAT'. Must be one of: stylish, json, sarif"
exit 1
;;
esac
# Validate max retries
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
# Validate email and username for fix mode
if [ "$MODE" = "fix" ]; then
if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then
echo "::error::Invalid email format: '$EMAIL'. Expected valid email address"
exit 1
fi
# Validate username format (GitHub canonical rules)
username="$USERNAME"
if [ ${#username} -gt 39 ]; then
echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters"
exit 1
fi
if ! [[ "$username" =~ ^[a-zA-Z0-9-]+$ ]]; then
echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed"
exit 1
fi
if [[ "$username" == -* ]] || [[ "$username" == *- ]]; then
echo "::error::Invalid username '$username'. Cannot start or end with hyphen"
exit 1
fi
if [[ "$username" == *--* ]]; then
echo "::error::Invalid username '$username'. Consecutive hyphens not allowed"
exit 1
fi
fi
echo "Input validation completed successfully"
- name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
with:
token: ${{ inputs.token || github.token }}
- name: Node Setup
id: node-setup
uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42
- name: Cache Node Dependencies
id: cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules
key: ${{ runner.os }}-eslint-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
restore-keys: |
${{ runner.os }}-eslint-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}-
${{ runner.os }}-eslint-lint-${{ inputs.mode }}-
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
env:
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
run: |
set -euo pipefail
echo "Installing dependencies using $PACKAGE_MANAGER..."
case "$PACKAGE_MANAGER" in
"pnpm")
pnpm install --frozen-lockfile
;;
"yarn")
if [ -f ".yarnrc.yml" ]; then
yarn install --immutable
else
yarn install --frozen-lockfile
fi
;;
"bun")
bun install --frozen-lockfile
;;
"npm"|*)
npm ci
;;
esac
echo "✅ Dependencies installed successfully"
- name: Run ESLint Check
if: inputs.mode == 'check'
id: check
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
ESLINT_VERSION: ${{ inputs.eslint-version }}
CONFIG_FILE: ${{ inputs.config-file }}
CACHE: ${{ inputs.cache }}
MAX_WARNINGS: ${{ inputs.max-warnings }}
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
REPORT_FORMAT: ${{ inputs.report-format }}
FILE_EXTENSIONS: ${{ inputs.file-extensions }}
run: |
set -euo pipefail
echo "Running ESLint check mode..."
# Build ESLint command
eslint_cmd="npx eslint ."
# Add config file if specified
if [ "$CONFIG_FILE" != ".eslintrc" ] && [ -f "$CONFIG_FILE" ]; then
eslint_cmd="$eslint_cmd --config $CONFIG_FILE"
fi
# Add cache option
if [ "$CACHE" = "true" ]; then
eslint_cmd="$eslint_cmd --cache"
fi
# Add max warnings
eslint_cmd="$eslint_cmd --max-warnings $MAX_WARNINGS"
# Add format
if [ "$REPORT_FORMAT" = "sarif" ]; then
eslint_cmd="$eslint_cmd --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif"
else
eslint_cmd="$eslint_cmd --format $REPORT_FORMAT"
fi
# Run ESLint and capture exit code
eslint_exit_code=0
eval "$eslint_cmd" || eslint_exit_code=$?
# Parse results
if [ "$REPORT_FORMAT" = "sarif" ] && [ -f eslint-results.sarif ]; then
error_count=$(jq '[.runs[]?.results[]? | select(.level == "error")] | length' eslint-results.sarif 2>/dev/null || echo "0")
warning_count=$(jq '[.runs[]?.results[]? | select(.level == "warning")] | length' eslint-results.sarif 2>/dev/null || echo "0")
files_checked=$(jq '[.runs[]?.results[]?.locations[]?.physicalLocation?.artifactLocation?.uri] | unique | length' eslint-results.sarif 2>/dev/null || echo "0")
sarif_file="eslint-results.sarif"
else
error_count="0"
warning_count="0"
files_checked="0"
sarif_file=""
fi
# Set outputs
if [ $eslint_exit_code -eq 0 ]; then
echo "status=success" >> "$GITHUB_OUTPUT"
else
echo "status=failure" >> "$GITHUB_OUTPUT"
fi
echo "error_count=$error_count" >> "$GITHUB_OUTPUT"
echo "warning_count=$warning_count" >> "$GITHUB_OUTPUT"
echo "files_checked=$files_checked" >> "$GITHUB_OUTPUT"
echo "sarif_file=$sarif_file" >> "$GITHUB_OUTPUT"
echo "✅ ESLint check completed: $error_count errors, $warning_count warnings"
# Exit with eslint's exit code if fail-on-error is true
if [ "$FAIL_ON_ERROR" = "true" ]; then
exit $eslint_exit_code
fi
- name: Upload SARIF Report
if: inputs.mode == 'check' && inputs.report-format == 'sarif' && always()
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
with:
sarif_file: ${{ inputs.working-directory }}/eslint-results.sarif
- name: Run ESLint Fix
if: inputs.mode == 'fix'
id: fix
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
run: |
set -euo pipefail
echo "Running ESLint fix mode..."
# Count files before fix
files_before=$(git status --porcelain | wc -l | tr -d ' ')
# Run ESLint fix based on package manager
case "$PACKAGE_MANAGER" in
"pnpm")
pnpm exec eslint . --fix || true
;;
"yarn")
yarn eslint . --fix || true
;;
"bun")
bunx eslint . --fix || true
;;
"npm"|*)
npx eslint . --fix || true
;;
esac
# Count files after fix
files_after=$(git status --porcelain | wc -l | tr -d ' ')
files_changed=$((files_after - files_before))
# Get number of errors fixed (approximate from diff)
errors_fixed=$(git diff --numstat | wc -l | tr -d ' ')
printf '%s\n' "files_changed=$files_changed" >> "$GITHUB_OUTPUT"
printf '%s\n' "errors_fixed=$errors_fixed" >> "$GITHUB_OUTPUT"
printf '%s\n' "status=success" >> "$GITHUB_OUTPUT"
echo "✅ ESLint fix completed. Files changed: $files_changed, Errors fixed: $errors_fixed"
- name: Commit and Push Fixes
if: inputs.mode == 'fix' && success()
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
with:
commit_message: 'style: autofix ESLint violations'
commit_user_name: ${{ inputs.username }}
commit_user_email: ${{ inputs.email }}
add_options: '-u'