# 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: Biome Lint description: Run Biome linter in check or fix mode author: Ismo Vuorinen branding: icon: check-circle color: green inputs: mode: description: 'Mode to run (check or fix)' required: false default: 'check' 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' max-retries: description: 'Maximum number of retry attempts for npm install operations' required: false default: '3' fail-on-error: description: 'Whether to fail the action if linting errors are found (check mode only)' required: false default: 'true' outputs: status: description: 'Overall status (success/failure)' value: ${{ steps.check.outputs.status || steps.fix.outputs.status }} errors_count: description: 'Number of errors found (check mode only)' value: ${{ steps.check.outputs.errors }} warnings_count: description: 'Number of warnings found (check mode only)' value: ${{ steps.check.outputs.warnings }} files_changed: description: 'Number of files changed (fix mode only)' value: ${{ steps.fix.outputs.files_changed }} runs: using: composite steps: - name: Validate Inputs id: validate shell: sh env: MODE: ${{ inputs.mode }} GITHUB_TOKEN: ${{ inputs.token }} EMAIL: ${{ inputs.email }} USERNAME: ${{ inputs.username }} MAX_RETRIES: ${{ inputs.max-retries }} FAIL_ON_ERROR: ${{ inputs.fail-on-error }} 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 GitHub token presence if provided if [ -n "$GITHUB_TOKEN" ]; then case "$GITHUB_TOKEN" in \$\{\{*) # Token is a GitHub Actions expression, skip validation ;; *) echo "Using provided GitHub token" ;; esac fi # Validate email format (basic check) - required for fix mode if [ "$MODE" = "fix" ]; then case "$EMAIL" in *@*.*) ;; *) echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" exit 1 ;; esac # Validate username format (GitHub canonical rules) username="$USERNAME" # Check length (GitHub limit) if [ ${#username} -gt 39 ]; then echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" exit 1 fi # Check allowed characters (letters, digits, hyphens only) case "$username" in *[!a-zA-Z0-9-]*) echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" exit 1 ;; esac # Check doesn't start or end with hyphen case "$username" in -*|*-) echo "::error::Invalid username '$username'. Cannot start or end with hyphen" exit 1 ;; esac # Check no consecutive hyphens case "$username" in *--*) echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" exit 1 ;; esac fi # Validate max retries (positive integer with reasonable upper limit) 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 fail-on-error (boolean) if [ "$FAIL_ON_ERROR" != "true" ] && [ "$FAIL_ON_ERROR" != "false" ]; then echo "::error::Invalid fail-on-error value: '$FAIL_ON_ERROR'. Must be 'true' or 'false'" exit 1 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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: '22' - 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 }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} restore-keys: | ${{ runner.os }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}- ${{ runner.os }}-biome-lint-${{ inputs.mode }}- - name: Install Biome shell: sh env: PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} MAX_RETRIES: ${{ inputs.max-retries }} run: | set -eu # Check if biome is already installed if command -v biome >/dev/null 2>&1; then echo "✅ Biome already installed: $(biome --version)" exit 0 fi echo "Installing Biome using $PACKAGE_MANAGER..." for attempt in $(seq 1 "$MAX_RETRIES"); do echo "Attempt $attempt of $MAX_RETRIES" case "$PACKAGE_MANAGER" in "pnpm") if pnpm add -g @biomejs/biome; then echo "✅ Biome installed successfully with pnpm" exit 0 fi ;; "yarn") if yarn global add @biomejs/biome; then echo "✅ Biome installed successfully with yarn" exit 0 fi ;; "bun") if bun add -g @biomejs/biome; then echo "✅ Biome installed successfully with bun" exit 0 fi ;; "npm"|*) if npm install -g @biomejs/biome; then echo "✅ Biome installed successfully with npm" exit 0 fi ;; esac if [ $attempt -lt "$MAX_RETRIES" ]; then echo "❌ Installation failed, retrying in 5 seconds..." sleep 5 fi done echo "::error::Failed to install Biome after $MAX_RETRIES attempts" exit 1 - name: Run Biome Check if: inputs.mode == 'check' id: check shell: sh env: FAIL_ON_ERROR: ${{ inputs.fail-on-error }} run: | set -eu echo "Running Biome check mode..." # Run Biome check with SARIF reporter biome_exit_code=0 biome check . --reporter=sarif > biome-report.sarif || biome_exit_code=$? # Handle failures gracefully if [ $biome_exit_code -ne 0 ] && [ ! -s biome-report.sarif ]; then echo "::warning::SARIF report generation failed with exit code $biome_exit_code" # Create empty SARIF file to avoid upload errors echo '{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[]}' > biome-report.sarif fi # Parse SARIF output for error counts if [ -f biome-report.sarif ]; then errors=$(jq '[.runs[]?.results[]? | select(.level == "error" or .level == "warning")] | length' biome-report.sarif 2>/dev/null || echo "0") warnings="0" # Biome doesn't separate warnings in SARIF output else errors="0" warnings="0" fi if [ $biome_exit_code -eq 0 ]; then echo "status=success" >> "$GITHUB_OUTPUT" echo "errors=0" >> "$GITHUB_OUTPUT" echo "warnings=0" >> "$GITHUB_OUTPUT" echo "✅ Biome check completed successfully" else echo "status=failure" >> "$GITHUB_OUTPUT" echo "errors=$errors" >> "$GITHUB_OUTPUT" echo "warnings=$warnings" >> "$GITHUB_OUTPUT" echo "::error::Biome check found $errors issues" fi # Exit with biome's exit code if fail-on-error is true if [ "$FAIL_ON_ERROR" = "true" ]; then exit $biome_exit_code fi - name: Upload SARIF Report if: inputs.mode == 'check' && always() uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: sarif_file: biome-report.sarif - name: Run Biome Fix if: inputs.mode == 'fix' id: fix shell: sh run: | set -eu echo "Running Biome fix mode..." # Run Biome fix and capture exit code biome_exit_code=0 biome check --write . || biome_exit_code=$? # Count changed files using git diff files_changed=$(git diff --name-only | wc -l | tr -d ' ') # Set status based on biome check result and changes if [ $biome_exit_code -eq 0 ] && [ "$files_changed" -eq 0 ]; then status="success" echo "✅ No changes needed" else status="failure" echo "⚠️ Fixed $files_changed file(s)" fi printf '%s\n' "files_changed=$files_changed" >> "$GITHUB_OUTPUT" printf '%s\n' "status=$status" >> "$GITHUB_OUTPUT" - 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 Biome violations' commit_user_name: ${{ inputs.username }} commit_user_email: ${{ inputs.email }} add_options: '-u'