mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
517 lines
17 KiB
YAML
517 lines
17 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: sh
|
|
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 -eu
|
|
|
|
# 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)
|
|
case "$WORKING_DIRECTORY" in
|
|
*..*)
|
|
echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Validate ESLint version format
|
|
if [ -n "$ESLINT_VERSION" ] && [ "$ESLINT_VERSION" != "latest" ]; then
|
|
if ! echo "$ESLINT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9]+([.-][a-zA-Z0-9]+)*)?$'; then
|
|
echo "::error::Invalid eslint-version format: '$ESLINT_VERSION'. Expected format: X.Y.Z or X.Y.Z-prerelease or 'latest' (e.g., 8.57.0, 8.57.0-rc.1, latest)"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Validate config file path if not default
|
|
if [ "$CONFIG_FILE" != ".eslintrc" ]; then
|
|
case "$CONFIG_FILE" in
|
|
*..*)
|
|
echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed"
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Validate ignore file path if not default
|
|
if [ "$IGNORE_FILE" != ".eslintignore" ]; then
|
|
case "$IGNORE_FILE" in
|
|
*..*)
|
|
echo "::error::Invalid ignore file path: '$IGNORE_FILE'. Path traversal not allowed"
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Validate file extensions format (must start with . and contain letters/numbers)
|
|
if ! echo "$FILE_EXTENSIONS" | grep -Eq '^\.[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() {
|
|
value="$1"
|
|
name="$2"
|
|
|
|
case "$value" in
|
|
true|True|TRUE|false|False|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 (non-negative integer)
|
|
case "$MAX_WARNINGS" in
|
|
''|*[!0-9]*)
|
|
echo "::error::Invalid max-warnings: '$MAX_WARNINGS'. Must be a non-negative integer"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# 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
|
|
case "$MAX_RETRIES" in
|
|
''|*[!0-9]*)
|
|
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [ "$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 ! echo "$EMAIL" | grep -Eq '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
|
|
echo "::error::Invalid email format: '$EMAIL'. Expected valid email address (e.g., user@example.com)"
|
|
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
|
|
|
|
case "$username" in
|
|
*[!a-zA-Z0-9-]*)
|
|
echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "$username" in
|
|
-*|*-)
|
|
echo "::error::Invalid username '$username'. Cannot start or end with hyphen"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "$username" in
|
|
*--*)
|
|
echo "::error::Invalid username '$username'. Consecutive hyphens not allowed"
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo "Input validation completed successfully"
|
|
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
|
with:
|
|
token: ${{ inputs.token || github.token }}
|
|
|
|
- name: Detect Package Manager
|
|
id: detect-pm
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
|
|
# Detect package manager from lockfiles
|
|
if [ -f bun.lockb ]; then
|
|
package_manager="bun"
|
|
elif [ -f pnpm-lock.yaml ]; then
|
|
package_manager="pnpm"
|
|
elif [ -f yarn.lock ]; then
|
|
package_manager="yarn"
|
|
else
|
|
package_manager="npm"
|
|
fi
|
|
|
|
printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT"
|
|
echo "Detected package manager: $package_manager"
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
|
with:
|
|
node-version: '24'
|
|
|
|
- name: Enable Corepack
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
corepack enable
|
|
|
|
- name: Install Package Manager
|
|
shell: sh
|
|
env:
|
|
PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }}
|
|
run: |
|
|
set -eu
|
|
|
|
case "$PACKAGE_MANAGER" in
|
|
pnpm)
|
|
corepack prepare pnpm@latest --activate
|
|
;;
|
|
yarn)
|
|
corepack prepare yarn@stable --activate
|
|
;;
|
|
bun|npm)
|
|
# Bun installed separately, npm built-in
|
|
;;
|
|
esac
|
|
|
|
- name: Setup Bun
|
|
if: steps.detect-pm.outputs.package-manager == 'bun'
|
|
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
|
with:
|
|
bun-version: latest
|
|
|
|
- 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.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-eslint-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-
|
|
${{ runner.os }}-eslint-lint-${{ inputs.mode }}-
|
|
|
|
- name: Install Dependencies
|
|
if: steps.cache.outputs.cache-hit != 'true'
|
|
shell: sh
|
|
env:
|
|
PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }}
|
|
run: |
|
|
set -eu
|
|
|
|
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: sh
|
|
working-directory: ${{ inputs.working-directory }}
|
|
env:
|
|
PACKAGE_MANAGER: ${{ steps.detect-pm.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 -eu
|
|
|
|
echo "Running ESLint check mode..."
|
|
|
|
# Build ESLint command based on package manager
|
|
case "$PACKAGE_MANAGER" in
|
|
"pnpm")
|
|
eslint_cmd="pnpm exec eslint . --ext $FILE_EXTENSIONS"
|
|
;;
|
|
"yarn")
|
|
eslint_cmd="yarn eslint . --ext $FILE_EXTENSIONS"
|
|
;;
|
|
"bun")
|
|
eslint_cmd="bunx eslint . --ext $FILE_EXTENSIONS"
|
|
;;
|
|
"npm"|*)
|
|
eslint_cmd="npx eslint . --ext $FILE_EXTENSIONS"
|
|
;;
|
|
esac
|
|
|
|
# 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@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
|
with:
|
|
sarif_file: ${{ inputs.working-directory }}/eslint-results.sarif
|
|
|
|
- name: Run ESLint Fix
|
|
if: inputs.mode == 'fix'
|
|
id: fix
|
|
shell: sh
|
|
working-directory: ${{ inputs.working-directory }}
|
|
env:
|
|
PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }}
|
|
FILE_EXTENSIONS: ${{ inputs.file-extensions }}
|
|
run: |
|
|
set -eu
|
|
|
|
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 . --ext $FILE_EXTENSIONS --fix || true
|
|
;;
|
|
"yarn")
|
|
yarn eslint . --ext $FILE_EXTENSIONS --fix || true
|
|
;;
|
|
"bun")
|
|
bunx eslint . --ext $FILE_EXTENSIONS --fix || true
|
|
;;
|
|
"npm"|*)
|
|
npx eslint . --ext $FILE_EXTENSIONS --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'
|