# yaml-language-server: $schema=https://json.schemastore.org/github-action.json # permissions: # - contents: write # Required for committing linter fixes # - pull-requests: write # Required for creating pull requests with fixes --- # MegaLinter GitHub Action configuration file # More info at https://megalinter.io name: PR Lint description: Runs MegaLinter against pull requests author: Ismo Vuorinen branding: icon: check-circle color: green inputs: token: description: 'GitHub token for authentication' required: false default: '' username: description: 'GitHub username for commits' required: false default: 'github-actions' email: description: 'GitHub email for commits' required: false default: 'github-actions@github.com' outputs: validation_status: description: 'Overall validation status (success/failure)' value: ${{ steps.ml.outputs.has_updated_sources == '1' && 'failure' || 'success' }} errors_found: description: 'Number of linting errors found' value: ${{ steps.ml.outputs.has_updated_sources }} runs: using: composite steps: - name: Validate Inputs id: validate uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb with: action-type: pr-lint token: ${{ inputs.token }} username: ${{ inputs.username }} email: ${{ inputs.email }} # ╭──────────────────────────────────────────────────────────╮ # │ Git Checkout │ # ╰──────────────────────────────────────────────────────────╯ - name: Checkout Code uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta with: token: ${{ inputs.token || github.token }} ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false # ╭──────────────────────────────────────────────────────────╮ # │ Install packages for linting │ # ╰──────────────────────────────────────────────────────────╯ # Node.js tests if package.json exists - name: Detect package.json id: detect-node shell: sh run: | set -eu if [ -f package.json ]; then printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" # Check if packageManager field is set (for corepack) if command -v jq >/dev/null 2>&1; then has_package_manager=$(jq -r '.packageManager // empty' package.json 2>/dev/null || printf '') if [ -n "$has_package_manager" ]; then printf '%s\n' "has-package-manager=true" >> "$GITHUB_OUTPUT" printf 'Found packageManager field: %s\n' "$has_package_manager" else printf '%s\n' "has-package-manager=false" >> "$GITHUB_OUTPUT" fi else # Fallback: check with grep if jq not available # Use robust pattern to verify non-empty value if grep -q '"packageManager"[[:space:]]*:[[:space:]]*"[^"]\+"' package.json 2>/dev/null; then printf '%s\n' "has-package-manager=true" >> "$GITHUB_OUTPUT" printf '%s\n' "Found packageManager field in package.json" else printf '%s\n' "has-package-manager=false" >> "$GITHUB_OUTPUT" fi fi else # Explicitly set has-package-manager to false when package.json doesn't exist printf '%s\n' "has-package-manager=false" >> "$GITHUB_OUTPUT" fi - name: Detect Package Manager if: steps.detect-node.outputs.found == 'true' 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" printf 'Detected package manager: %s\n' "$package_manager" - name: Setup Node.js if: steps.detect-node.outputs.found == 'true' uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '24' - name: Enable Corepack if: steps.detect-node.outputs.found == 'true' && steps.detect-node.outputs.has-package-manager == 'true' shell: sh run: | set -eu corepack enable printf '%s\n' "Corepack enabled - package manager will be installed automatically from package.json" - name: Install Package Manager (Fallback) if: steps.detect-node.outputs.found == 'true' && steps.detect-node.outputs.has-package-manager == 'false' shell: sh env: PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} run: | set -eu printf 'No packageManager field found, using detected package manager: %s\n' "$PACKAGE_MANAGER" case "$PACKAGE_MANAGER" in pnpm) corepack enable corepack prepare pnpm@latest --activate ;; yarn) corepack enable corepack prepare yarn@stable --activate ;; bun|npm) # Bun installed separately, npm built-in ;; esac - name: Setup Bun if: steps.detect-node.outputs.found == 'true' && steps.detect-pm.outputs.package-manager == 'bun' uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 with: bun-version: latest - name: Cache Node Dependencies if: steps.detect-node.outputs.found == 'true' id: node-cache uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: node_modules key: ${{ runner.os }}-pr-lint-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} restore-keys: | ${{ runner.os }}-pr-lint-${{ steps.detect-pm.outputs.package-manager }}- - name: Install Node Dependencies if: steps.detect-node.outputs.found == 'true' && steps.node-cache.outputs.cache-hit != 'true' shell: sh env: PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} run: | set -eu printf 'Installing dependencies using %s...\n' "$PACKAGE_MANAGER" case "$PACKAGE_MANAGER" in "pnpm") pnpm install --frozen-lockfile ;; "yarn") # Detect Yarn version by checking actual version output # Yarn 2+ (Berry) uses --immutable, Yarn 1.x (Classic) uses --frozen-lockfile yarn_version=$(yarn --version 2>/dev/null || printf '1.0.0') if printf '%s' "$yarn_version" | grep -q '^[2-9]'; then # Yarn 2+ (Berry) - use --immutable yarn install --immutable else # Yarn 1.x (Classic) - use --frozen-lockfile yarn install --frozen-lockfile fi ;; "bun") bun install --frozen-lockfile ;; "npm"|*) npm ci ;; esac printf '✅ Dependencies installed successfully\n' # PHP tests if composer.json exists - name: Detect composer.json id: detect-php shell: sh run: | set -eu if [ -f composer.json ]; then printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Detect PHP Version if: steps.detect-php.outputs.found == 'true' id: php-version shell: sh env: DEFAULT_VERSION: '8.4' run: | set -eu # Function to validate version format validate_version() { version=$1 case "$version" in [0-9]*\.[0-9]* | [0-9]*\.[0-9]*\.[0-9]*) return 0 ;; *) return 1 ;; esac } # Function to clean version string clean_version() { printf '%s' "$1" | sed 's/^[vV]//' | tr -d ' \n\r' } detected_version="" # Parse .tool-versions file if [ -f .tool-versions ]; then printf 'Checking .tool-versions for php...\n' >&2 version=$(awk '/^php[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found PHP version in .tool-versions: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse Dockerfile if [ -z "$detected_version" ] && [ -f Dockerfile ]; then printf 'Checking Dockerfile for php...\n' >&2 version=$(grep -iF "FROM" Dockerfile | grep -F "php:" | head -1 | \ sed -n -E "s/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found PHP version in Dockerfile: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse devcontainer.json if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then printf 'Checking devcontainer.json for php...\n' >&2 if command -v jq >/dev/null 2>&1; then version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found PHP version in devcontainer: %s\n' "$version" >&2 detected_version="$version" fi fi else printf 'jq not found; skipping devcontainer.json parsing\n' >&2 fi fi # Parse .php-version file if [ -z "$detected_version" ] && [ -f .php-version ]; then printf 'Checking .php-version...\n' >&2 version=$(tr -d '\r' < .php-version | head -1) if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found PHP version in .php-version: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse composer.json if [ -z "$detected_version" ] && [ -f composer.json ]; then printf 'Checking composer.json...\n' >&2 if command -v jq >/dev/null 2>&1; then version=$(jq -r '.require.php // empty' composer.json 2>/dev/null | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p') if [ -z "$version" ]; then version=$(jq -r '.config.platform.php // empty' composer.json 2>/dev/null | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p') fi if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found PHP version in composer.json: %s\n' "$version" >&2 detected_version="$version" fi fi else printf 'jq not found; skipping composer.json parsing\n' >&2 fi fi # Use default version if nothing detected if [ -z "$detected_version" ]; then detected_version="$DEFAULT_VERSION" printf 'Using default PHP version: %s\n' "$detected_version" >&2 fi # Set output printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" printf 'Final detected PHP version: %s\n' "$detected_version" >&2 - name: Setup PHP if: steps.detect-php.outputs.found == 'true' uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0 with: php-version: ${{ steps.php-version.outputs.detected-version }} tools: composer coverage: none env: GITHUB_TOKEN: ${{ inputs.token || github.token }} - name: Setup problem matchers for PHP if: steps.detect-php.outputs.found == 'true' shell: sh env: RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | set -eu matcher_path=$(printf '%s' "$RUNNER_TOOL_CACHE/php.json" | tr -d '\n\r') printf '%s\n' "::add-matcher::$matcher_path" - name: Install PHP dependencies if: steps.detect-php.outputs.found == 'true' shell: sh run: | set -eu composer install --no-progress --prefer-dist --no-interaction # Python tests if requirements.txt exists - name: Detect requirements.txt id: detect-python shell: sh run: | set -eu if [ -f requirements.txt ]; then printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Detect Python Version if: steps.detect-python.outputs.found == 'true' id: python-version shell: sh env: DEFAULT_VERSION: '3.14' run: | set -eu # Function to validate version format validate_version() { version=$1 case "$version" in [0-9]*\.[0-9]* | [0-9]*\.[0-9]*\.[0-9]*) return 0 ;; *) return 1 ;; esac } # Function to clean version string clean_version() { printf '%s' "$1" | sed 's/^[vV]//' | tr -d ' \n\r' } detected_version="" # Parse .tool-versions file if [ -f .tool-versions ]; then printf 'Checking .tool-versions for python...\n' >&2 version=$(awk '/^python[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Python version in .tool-versions: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse Dockerfile if [ -z "$detected_version" ] && [ -f Dockerfile ]; then printf 'Checking Dockerfile for python...\n' >&2 version=$(grep -iF "FROM" Dockerfile | grep -F "python:" | head -1 | \ sed -n -E "s/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Python version in Dockerfile: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse devcontainer.json if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then printf 'Checking devcontainer.json for python...\n' >&2 if command -v jq >/dev/null 2>&1; then version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Python version in devcontainer: %s\n' "$version" >&2 detected_version="$version" fi fi else printf 'jq not found; skipping devcontainer.json parsing\n' >&2 fi fi # Parse .python-version file if [ -z "$detected_version" ] && [ -f .python-version ]; then printf 'Checking .python-version...\n' >&2 version=$(tr -d '\r' < .python-version | head -1) if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Python version in .python-version: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse pyproject.toml if [ -z "$detected_version" ] && [ -f pyproject.toml ]; then printf 'Checking pyproject.toml...\n' >&2 if grep -q '^\[project\]' pyproject.toml; then version=$(grep -A 20 '^\[project\]' pyproject.toml | grep -E '^\s*requires-python[[:space:]]*=' | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p' | head -1) if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Python version in pyproject.toml: %s\n' "$version" >&2 detected_version="$version" fi fi fi fi # Use default version if nothing detected if [ -z "$detected_version" ]; then detected_version="$DEFAULT_VERSION" printf 'Using default Python version: %s\n' "$detected_version" >&2 fi # Set output printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" printf 'Final detected Python version: %s\n' "$detected_version" >&2 - name: Setup Python if: steps.detect-python.outputs.found == 'true' uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ steps.python-version.outputs.detected-version }} cache: 'pip' - name: Install Python dependencies if: steps.detect-python.outputs.found == 'true' shell: sh run: | set -eu pip install -r requirements.txt # Go tests if go.mod exists - name: Detect go.mod id: detect-go shell: sh run: | set -eu if [ -f go.mod ]; then printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Detect Go Version if: steps.detect-go.outputs.found == 'true' id: go-version shell: sh env: DEFAULT_VERSION: '1.25' run: | set -eu # Function to validate version format validate_version() { version=$1 case "$version" in [0-9]*\.[0-9]* | [0-9]*\.[0-9]*\.[0-9]*) return 0 ;; *) return 1 ;; esac } # Function to clean version string clean_version() { printf '%s' "$1" | sed 's/^[vV]//' | tr -d ' \n\r' } detected_version="" # Parse .tool-versions file if [ -f .tool-versions ]; then printf 'Checking .tool-versions for golang...\n' >&2 version=$(awk '/^golang[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Go version in .tool-versions: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse Dockerfile if [ -z "$detected_version" ] && [ -f Dockerfile ]; then printf 'Checking Dockerfile for golang...\n' >&2 version=$(grep -iF "FROM" Dockerfile | grep -F "golang:" | head -1 | \ sed -n -E "s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Go version in Dockerfile: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse devcontainer.json if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then printf 'Checking devcontainer.json for golang...\n' >&2 if command -v jq >/dev/null 2>&1; then version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Go version in devcontainer: %s\n' "$version" >&2 detected_version="$version" fi fi else printf 'jq not found; skipping devcontainer.json parsing\n' >&2 fi fi # Parse .go-version file if [ -z "$detected_version" ] && [ -f .go-version ]; then printf 'Checking .go-version...\n' >&2 version=$(tr -d '\r' < .go-version | head -1) if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Go version in .go-version: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Parse go.mod if [ -z "$detected_version" ] && [ -f go.mod ]; then printf 'Checking go.mod...\n' >&2 version=$(grep -E '^go[[:space:]]+[0-9]' go.mod | awk '{print $2}' | head -1 || echo "") if [ -n "$version" ]; then version=$(clean_version "$version") if validate_version "$version"; then printf 'Found Go version in go.mod: %s\n' "$version" >&2 detected_version="$version" fi fi fi # Use default version if nothing detected if [ -z "$detected_version" ]; then detected_version="$DEFAULT_VERSION" printf 'Using default Go version: %s\n' "$detected_version" >&2 fi # Set output printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" printf 'Final detected Go version: %s\n' "$detected_version" >&2 - name: Setup Go if: steps.detect-go.outputs.found == 'true' uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ steps.go-version.outputs.detected-version }} cache: true # ╭──────────────────────────────────────────────────────────╮ # │ MegaLinter │ # ╰──────────────────────────────────────────────────────────╯ - name: MegaLinter # You can override MegaLinter flavor used to have faster performances # More info at https://megalinter.io/latest/flavors/ uses: oxsecurity/megalinter/flavors/cupcake@42bb470545e359597e7f12156947c436e4e3fb9a # v9.3.0 id: ml # All available variables are described in documentation # https://megalinter.io/latest/configuration/ env: # Validates all source when push on main, else just the git diff with # main. Override with true if you always want to lint all sources # # To validate the entire codebase, set to: # VALIDATE_ALL_CODEBASE: true # # To validate only diff with main, set to: # VALIDATE_ALL_CODEBASE: >- # ${{ # github.event_name == 'push' && # contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref) # }} VALIDATE_ALL_CODEBASE: false GITHUB_TOKEN: ${{ inputs.token || github.token }} # Apply linter fixes configuration # # When active, APPLY_FIXES must also be defined as environment variable # (in .github/workflows/mega-linter.yml or other CI tool) APPLY_FIXES: none # Decide which event triggers application of fixes in a commit or a PR # (pull_request, push, all) APPLY_FIXES_EVENT: pull_request # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) # or posted in a PR (pull_request) APPLY_FIXES_MODE: commit # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE # .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY # Uncomment to disable copy-paste and spell checks DISABLE: COPYPASTE,SPELL # Upload MegaLinter artifacts - name: Archive production artifacts if: success() || failure() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: MegaLinter reports include-hidden-files: 'true' path: | megalinter-reports mega-linter.log