# 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" < "$IGNORE_FILE" < "$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