Files
actions/prettier-check/action.yml
Ismo Vuorinen 7061aafd35 chore: add tests, update docs and actions (#299)
* docs: update documentation

* feat: validate-inputs has it's own pyproject

* security: mask DOCKERHUB_PASSWORD

* chore: add tokens, checkout, recrete docs, integration tests

* fix: add `statuses: write` permission to pr-lint
2025-10-18 13:09:19 +03:00

459 lines
14 KiB
YAML

# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
# permissions:
# - contents: read # Required for reading repository files
# - security-events: write # Required for uploading SARIF reports
---
name: Prettier Check
description: 'Run Prettier check on the repository with advanced configuration and reporting'
author: Ismo Vuorinen
branding:
icon: check-circle
color: green
inputs:
working-directory:
description: 'Directory containing files to check'
required: false
default: '.'
prettier-version:
description: 'Prettier version to use'
required: false
default: 'latest'
config-file:
description: 'Path to Prettier config file'
required: false
default: '.prettierrc'
ignore-file:
description: 'Path to Prettier ignore file'
required: false
default: '.prettierignore'
file-pattern:
description: 'Files to include (glob pattern)'
required: false
default: '**/*.{js,jsx,ts,tsx,css,scss,json,md,yaml,yml}'
cache:
description: 'Enable Prettier caching'
required: false
default: 'true'
fail-on-error:
description: 'Fail workflow if issues are found'
required: false
default: 'true'
report-format:
description: 'Output format (json, sarif)'
required: false
default: 'sarif'
max-retries:
description: 'Maximum number of retry attempts'
required: false
default: '3'
plugins:
description: 'Comma-separated list of Prettier plugins to install'
required: false
default: ''
check-only:
description: 'Only check for formatting issues without fixing'
required: false
default: 'true'
token:
description: 'GitHub token for authentication'
required: false
default: ''
outputs:
files-checked:
description: 'Number of files checked'
value: ${{ steps.check.outputs.files_checked }}
unformatted-files:
description: 'Number of files with formatting issues'
value: ${{ steps.check.outputs.unformatted_files }}
sarif-file:
description: 'Path to SARIF report file'
value: ${{ steps.check.outputs.sarif_file }}
cache-hit:
description: 'Indicates if there was a cache hit'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Validate Inputs
id: validate
shell: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
PRETTIER_VERSION: ${{ inputs.prettier-version }}
CONFIG_FILE: ${{ inputs.config-file }}
IGNORE_FILE: ${{ inputs.ignore-file }}
FILE_PATTERN: ${{ inputs.file-pattern }}
CACHE: ${{ inputs.cache }}
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
CHECK_ONLY: ${{ inputs.check-only }}
REPORT_FORMAT: ${{ inputs.report-format }}
MAX_RETRIES: ${{ inputs.max-retries }}
PLUGINS: ${{ inputs.plugins }}
run: |
set -euo pipefail
# Validate working directory
if [ ! -d "$WORKING_DIRECTORY" ]; then
echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Directory does not exist"
exit 1
fi
# Validate path security (prevent path traversal)
if [[ "$WORKING_DIRECTORY" == *".."* ]]; then
echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Path traversal not allowed"
exit 1
fi
# Validate prettier version format
if [[ "$PRETTIER_VERSION" != "latest" ]]; then
if ! [[ "$PRETTIER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "::error::Invalid prettier-version: '$PRETTIER_VERSION'. Expected semantic version (e.g., 3.0.0) or 'latest'"
exit 1
fi
fi
# Validate config file path security
if [[ "$CONFIG_FILE" == *".."* ]] || [[ "$CONFIG_FILE" == "/"* ]]; then
echo "::error::Invalid config-file path: '$CONFIG_FILE'. Path traversal not allowed"
exit 1
fi
# Validate ignore file path security
if [[ "$IGNORE_FILE" == *".."* ]] || [[ "$IGNORE_FILE" == "/"* ]]; then
echo "::error::Invalid ignore-file path: '$IGNORE_FILE'. Path traversal not allowed"
exit 1
fi
# Validate file pattern format (basic safety check)
if [[ "$FILE_PATTERN" == *".."* ]] || [[ "$FILE_PATTERN" == "/"* ]]; then
echo "::error::Invalid file-pattern: '$FILE_PATTERN'. Absolute paths and path traversal not allowed"
exit 1
fi
# Validate boolean inputs
case "$CACHE" in
true|false) ;;
*)
echo "::error::Invalid cache value: '$CACHE'. Expected: true or false"
exit 1
;;
esac
case "$FAIL_ON_ERROR" in
true|false) ;;
*)
echo "::error::Invalid fail-on-error value: '$FAIL_ON_ERROR'. Expected: true or false"
exit 1
;;
esac
case "$CHECK_ONLY" in
true|false) ;;
*)
echo "::error::Invalid check-only value: '$CHECK_ONLY'. Expected: true or false"
exit 1
;;
esac
# Validate report format
case "$REPORT_FORMAT" in
json|sarif) ;;
*)
echo "::error::Invalid report-format: '$REPORT_FORMAT'. Expected: json or sarif"
exit 1
;;
esac
# Validate max-retries (positive integer)
if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ]; then
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer"
exit 1
fi
# Validate max-retries range
if [ "$MAX_RETRIES" -gt 10 ]; then
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Maximum allowed is 10"
exit 1
fi
# Validate plugins format if provided
if [ -n "$PLUGINS" ]; then
# Check for basic npm package name format and prevent command injection
if ! [[ "$PLUGINS" =~ ^[a-zA-Z0-9@/._,-]+$ ]]; then
echo "::error::Invalid plugins format: '$PLUGINS'. Use comma-separated npm package names (e.g., plugin1,@scope/plugin2)"
exit 1
fi
# Check for suspicious patterns
if [[ "$PLUGINS" == *";"* ]] || [[ "$PLUGINS" == *"&&"* ]] || [[ "$PLUGINS" == *"|"* ]]; then
echo "::error::Invalid plugins format: '$PLUGINS'. Command injection patterns not allowed"
exit 1
fi
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: Set up Cache
id: cache
uses: ./common-cache
if: inputs.cache == 'true'
with:
type: 'npm'
paths: 'node_modules/.cache/prettier,.prettier-cache'
key-prefix: 'prettier-${{ steps.node-setup.outputs.package-manager }}'
key-files: package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb,${{ inputs.config-file }}
restore-keys: '${{ runner.os }}-prettier-'
- name: Install Dependencies
shell: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
PRETTIER_VERSION: ${{ inputs.prettier-version }}
PLUGINS: ${{ inputs.plugins }}
MAX_RETRIES: ${{ inputs.max-retries }}
run: |
set -euo pipefail
cd "$WORKING_DIRECTORY"
# Function to install with retries
install_with_retries() {
local attempt=1
local max_attempts="$MAX_RETRIES"
while [ $attempt -le $max_attempts ]; do
echo "Installation attempt $attempt of $max_attempts"
# Install Prettier and base dependencies
if npm install \
"prettier@$PRETTIER_VERSION" \
@prettier/plugin-xml \
prettier-plugin-packagejson \
prettier-plugin-sh; then
# Install additional plugins if specified
if [ -n "$PLUGINS" ]; then
IFS=',' read -ra PLUGIN_ARRAY <<< "$PLUGINS"
for plugin in "${PLUGIN_ARRAY[@]}"; do
if ! npm install "$plugin"; then
return 1
fi
done
fi
return 0
fi
attempt=$((attempt + 1))
if [ $attempt -le $max_attempts ]; then
echo "Installation failed, waiting 10 seconds before retry..."
sleep 10
fi
done
echo "::error::Failed to install dependencies after $max_attempts attempts"
return 1
}
install_with_retries
- name: Prepare 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 Prettier configuration..."
cat > "$CONFIG_FILE" <<EOF
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "avoid",
"endOfLine": "lf"
}
EOF
fi
# Create default ignore file if none exists
if [ ! -f "$IGNORE_FILE" ]; then
echo "Creating default Prettier ignore file..."
cat > "$IGNORE_FILE" <<EOF
node_modules/
dist/
build/
coverage/
.git/
*.min.js
*.d.ts
EOF
fi
- name: Run Prettier Check
id: check
shell: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CHECK_ONLY: ${{ inputs.check-only }}
CONFIG_FILE: ${{ inputs.config-file }}
IGNORE_FILE: ${{ inputs.ignore-file }}
CACHE: ${{ inputs.cache }}
FILE_PATTERN: ${{ inputs.file-pattern }}
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 cache flags
cache_flags=""
if [ "$CACHE" = "true" ]; then
cache_flags="--cache --cache-location=.prettier-cache"
fi
# Function to convert Prettier output to SARIF
prettier_to_sarif() {
local input_file=$1
local output_file=$2
echo '{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "Prettier",
"informationUri": "https://prettier.io",
"rules": []
}
},
"results": [' > "$output_file"
local first=true
while IFS= read -r line; do
if [ "$first" = true ]; then
first=false
else
echo "," >> "$output_file"
fi
echo "{
\"level\": \"error\",
\"message\": {
\"text\": \"File is not formatted according to Prettier rules\"
},
\"locations\": [
{
\"physicalLocation\": {
\"artifactLocation\": {
\"uri\": \"$line\"
}
}
}
]
}" >> "$output_file"
done < "$input_file"
echo ']}]}' >> "$output_file"
}
# Run Prettier
echo "Running Prettier check..."
unformatted_files=$(mktemp)
if [ "$CHECK_ONLY" = "true" ]; then
npx prettier \
--check \
--config "$CONFIG_FILE" \
--ignore-path "$IGNORE_FILE" \
$cache_flags \
--no-error-on-unmatched-pattern \
"$FILE_PATTERN" 2>&1 | \
grep -oE '[^ ]+\.[a-zA-Z]+$' > "$unformatted_files" || true
else
npx prettier \
--write \
--list-different \
--config "$CONFIG_FILE" \
--ignore-path "$IGNORE_FILE" \
$cache_flags \
--no-error-on-unmatched-pattern \
"$FILE_PATTERN" > "$unformatted_files" || true
fi
# Count files
files_checked=$(find . -type f -name "$FILE_PATTERN" -not -path "*/node_modules/*" | wc -l)
unformatted_count=$(wc -l < "$unformatted_files")
echo "files_checked=${files_checked}" >> $GITHUB_OUTPUT
echo "unformatted_files=${unformatted_count}" >> $GITHUB_OUTPUT
# Generate SARIF report if requested
if [ "$REPORT_FORMAT" = "sarif" ]; then
prettier_to_sarif "$unformatted_files" "reports/prettier.sarif"
echo "sarif_file=reports/prettier.sarif" >> $GITHUB_OUTPUT
fi
# Clean up temporary file
rm "$unformatted_files"
# Exit with error if issues found and fail-on-error is true
if [ "$FAIL_ON_ERROR" = "true" ] && [ "$unformatted_count" -gt 0 ]; then
echo "::error::Found $unformatted_count files with formatting issues"
exit 1
fi
- name: Upload Prettier Results
if: always() && inputs.report-format == 'sarif'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
sarif_file: ${{ inputs.working-directory }}/reports/prettier.sarif
category: prettier
- name: Cleanup
if: always()
shell: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CACHE: ${{ inputs.cache }}
run: |-
set -euo pipefail
cd "$WORKING_DIRECTORY"
# Remove temporary files
rm -rf reports/
# Clean cache if exists and not being preserved
if [ "$CACHE" != "true" ]; then
rm -rf .prettier-cache
rm -rf node_modules/.cache/prettier
fi