mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +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>
389 lines
13 KiB
YAML
389 lines
13 KiB
YAML
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
|
|
# permissions:
|
|
# - contents: write # Required for committing and pushing lint fixes
|
|
# - security-events: write # Required for uploading SARIF reports
|
|
---
|
|
name: Python Lint and Fix
|
|
description: 'Lints and fixes Python files, commits changes, and uploads SARIF report.'
|
|
author: 'Ismo Vuorinen'
|
|
|
|
branding:
|
|
icon: 'code'
|
|
color: 'yellow'
|
|
|
|
inputs:
|
|
python-version:
|
|
description: 'Python version to use'
|
|
required: false
|
|
default: '3.11'
|
|
flake8-version:
|
|
description: 'Flake8 version to use'
|
|
required: false
|
|
default: '7.0.0'
|
|
autopep8-version:
|
|
description: 'Autopep8 version to use'
|
|
required: false
|
|
default: '2.0.4'
|
|
max-retries:
|
|
description: 'Maximum number of retry attempts for installations and linting'
|
|
required: false
|
|
default: '3'
|
|
working-directory:
|
|
description: 'Directory containing Python files to lint'
|
|
required: false
|
|
default: '.'
|
|
fail-on-error:
|
|
description: 'Whether to fail the action if linting errors are found'
|
|
required: false
|
|
default: 'true'
|
|
token:
|
|
description: 'GitHub token for authentication'
|
|
required: false
|
|
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:
|
|
lint-result:
|
|
description: 'Result of the linting process (success/failure)'
|
|
value: ${{ steps.lint.outputs.result }}
|
|
fixed-files:
|
|
description: 'Number of files that were fixed'
|
|
value: ${{ steps.fix.outputs.fixed_count }}
|
|
error-count:
|
|
description: 'Number of errors found'
|
|
value: ${{ steps.lint.outputs.error_count }}
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Validate Inputs
|
|
id: validate
|
|
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
|
|
with:
|
|
action-type: 'python-lint-fix'
|
|
token: ${{ inputs.token }}
|
|
email: ${{ inputs.email }}
|
|
username: ${{ inputs.username }}
|
|
python-version: ${{ inputs.python-version }}
|
|
flake8-version: ${{ inputs.flake8-version }}
|
|
autopep8-version: ${{ inputs.autopep8-version }}
|
|
working-directory: ${{ inputs.working-directory }}
|
|
max-retries: ${{ inputs.max-retries }}
|
|
fail-on-error: ${{ inputs.fail-on-error }}
|
|
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
|
with:
|
|
token: ${{ inputs.token || github.token }}
|
|
|
|
- name: Detect Python Version
|
|
id: python-version
|
|
shell: sh
|
|
env:
|
|
DEFAULT_VERSION: "${{ inputs.python-version || '3.11' }}"
|
|
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
|
|
echo "Checking .tool-versions for python..." >&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
|
|
echo "Found Python version in .tool-versions: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Parse Dockerfile
|
|
if [ -z "$detected_version" ] && [ -f Dockerfile ]; then
|
|
echo "Checking Dockerfile for python..." >&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
|
|
echo "Found Python version in Dockerfile: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Parse devcontainer.json
|
|
if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then
|
|
echo "Checking devcontainer.json for python..." >&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
|
|
echo "Found Python version in devcontainer: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
else
|
|
echo "jq not found; skipping devcontainer.json parsing" >&2
|
|
fi
|
|
fi
|
|
|
|
# Parse .python-version file
|
|
if [ -z "$detected_version" ] && [ -f .python-version ]; then
|
|
echo "Checking .python-version..." >&2
|
|
version=$(tr -d '\r' < .python-version | head -1)
|
|
if [ -n "$version" ]; then
|
|
version=$(clean_version "$version")
|
|
if validate_version "$version"; then
|
|
echo "Found Python version in .python-version: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Parse pyproject.toml
|
|
if [ -z "$detected_version" ] && [ -f pyproject.toml ]; then
|
|
echo "Checking pyproject.toml..." >&2
|
|
if grep -q '^\[project\]' pyproject.toml; then
|
|
version=$(grep -A 20 '^\[project\]' pyproject.toml | grep -E '^[[:space:]]*requires-python[[:space:]]*=' | sed -n -E '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
|
|
echo "Found Python version in pyproject.toml: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Use default version if nothing detected
|
|
if [ -z "$detected_version" ]; then
|
|
detected_version="$DEFAULT_VERSION"
|
|
echo "Using default Python version: $detected_version" >&2
|
|
fi
|
|
|
|
# Set output
|
|
printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT"
|
|
echo "Final detected Python version: $detected_version" >&2
|
|
|
|
- name: Detect Package Manager
|
|
id: package-manager
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
|
|
# Detect Python package manager based on lock files and config
|
|
package_manager="pip"
|
|
|
|
if [ -f "uv.lock" ]; then
|
|
# uv uses pip-compatible caching, so we use 'pip' as cache type
|
|
package_manager="pip"
|
|
echo "Detected uv (using pip-compatible caching)" >&2
|
|
elif [ -f "poetry.lock" ]; then
|
|
package_manager="poetry"
|
|
echo "Detected Poetry" >&2
|
|
elif [ -f "Pipfile.lock" ] || [ -f "Pipfile" ]; then
|
|
package_manager="pipenv"
|
|
echo "Detected Pipenv" >&2
|
|
elif [ -f "requirements.txt" ] || [ -f "requirements-dev.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then
|
|
package_manager="pip"
|
|
echo "Detected pip" >&2
|
|
else
|
|
package_manager="pip"
|
|
echo "No package manager detected, defaulting to pip" >&2
|
|
fi
|
|
|
|
printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT"
|
|
echo "Using package manager: $package_manager" >&2
|
|
|
|
- name: Setup Python (pip)
|
|
if: steps.package-manager.outputs.package-manager == 'pip'
|
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
|
with:
|
|
python-version: ${{ steps.python-version.outputs.detected-version }}
|
|
cache: 'pip'
|
|
cache-dependency-path: |
|
|
**/requirements.txt
|
|
**/requirements-dev.txt
|
|
**/pyproject.toml
|
|
**/setup.py
|
|
**/uv.lock
|
|
|
|
- name: Setup Python (pipenv)
|
|
if: steps.package-manager.outputs.package-manager == 'pipenv'
|
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
|
with:
|
|
python-version: ${{ steps.python-version.outputs.detected-version }}
|
|
cache: 'pipenv'
|
|
cache-dependency-path: |
|
|
**/Pipfile
|
|
**/Pipfile.lock
|
|
|
|
- name: Setup Python (poetry)
|
|
if: steps.package-manager.outputs.package-manager == 'poetry'
|
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
|
with:
|
|
python-version: ${{ steps.python-version.outputs.detected-version }}
|
|
cache: 'poetry'
|
|
cache-dependency-path: |
|
|
**/poetry.lock
|
|
**/pyproject.toml
|
|
|
|
- name: Check for Python Files
|
|
id: check-files
|
|
shell: sh
|
|
env:
|
|
WORKING_DIRECTORY: ${{ inputs.working-directory }}
|
|
run: |
|
|
set -eu
|
|
|
|
cd "$WORKING_DIRECTORY"
|
|
if ! find . -name "*.py" -type f -not -path "*/\.*" | grep -q .; then
|
|
echo "No Python files found. Skipping lint and fix."
|
|
printf '%s\n' "result=skipped" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
printf '%s\n' "result=found" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Install Dependencies
|
|
if: steps.check-files.outputs.result == 'found'
|
|
id: install
|
|
shell: sh
|
|
env:
|
|
MAX_RETRIES: ${{ inputs.max-retries }}
|
|
FLAKE8_VERSION: ${{ inputs.flake8-version }}
|
|
AUTOPEP8_VERSION: ${{ inputs.autopep8-version }}
|
|
run: |
|
|
set -eu
|
|
|
|
# Create virtual environment
|
|
python -m venv .venv
|
|
. .venv/bin/activate
|
|
|
|
# Install dependencies (pip has built-in retry logic)
|
|
pip install "flake8==$FLAKE8_VERSION"
|
|
pip install flake8-sarif==0.6.0
|
|
pip install "autopep8==$AUTOPEP8_VERSION"
|
|
|
|
# Verify installations
|
|
flake8 --version || exit 1
|
|
autopep8 --version || exit 1
|
|
|
|
- name: Run flake8
|
|
if: steps.check-files.outputs.result == 'found'
|
|
id: lint
|
|
shell: sh
|
|
env:
|
|
WORKING_DIRECTORY: ${{ inputs.working-directory }}
|
|
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
|
|
run: |
|
|
set -eu
|
|
|
|
. .venv/bin/activate
|
|
cd "$WORKING_DIRECTORY"
|
|
|
|
# Create temporary directory for reports
|
|
mkdir -p reports
|
|
|
|
# Run flake8 with error handling
|
|
error_count=0
|
|
if ! flake8 --format=sarif --output-file=reports/flake8.sarif .; then
|
|
error_count=$(grep -c "level\": \"error\"" reports/flake8.sarif || echo 0)
|
|
echo "Found $error_count linting errors"
|
|
printf '%s\n' "error_count=$error_count" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ "$FAIL_ON_ERROR" = "true" ]; then
|
|
echo "::error::Linting failed with $error_count errors"
|
|
printf '%s\n' "result=failure" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
printf '%s\n' "result=success" >> "$GITHUB_OUTPUT"
|
|
printf '%s\n' "error_count=$error_count" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Run autopep8 Fix
|
|
if: steps.check-files.outputs.result == 'found'
|
|
id: fix
|
|
shell: sh
|
|
env:
|
|
WORKING_DIRECTORY: ${{ inputs.working-directory }}
|
|
run: |
|
|
set -eu
|
|
|
|
. .venv/bin/activate
|
|
cd "$WORKING_DIRECTORY"
|
|
|
|
# Create temporary file for tracking changes
|
|
touch /tmp/changed_files
|
|
|
|
# Run autopep8 with change detection
|
|
find . -name "*.py" -type f -not -path "*/\.*" | while read -r file; do
|
|
if autopep8 --diff "$file" | grep -q '^[+-]'; then
|
|
autopep8 --in-place "$file"
|
|
echo "$file" >> /tmp/changed_files
|
|
fi
|
|
done
|
|
|
|
# Count fixed files
|
|
fixed_count=$(wc -l < /tmp/changed_files || echo 0)
|
|
echo "Fixed $fixed_count files"
|
|
printf '%s\n' "fixed_count=$fixed_count" >> "$GITHUB_OUTPUT"
|
|
|
|
# Cleanup
|
|
rm /tmp/changed_files
|
|
|
|
- name: Commit Fixes
|
|
if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }}
|
|
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
|
with:
|
|
commit_message: 'style: apply python lint fixes'
|
|
commit_user_name: ${{ inputs.username }}
|
|
commit_user_email: ${{ inputs.email }}
|
|
file_pattern: '*.py'
|
|
|
|
- name: Upload SARIF Report
|
|
if: steps.check-files.outputs.result == 'found'
|
|
uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
|
with:
|
|
sarif_file: ${{ inputs.working-directory }}/reports/flake8.sarif
|
|
category: 'python-lint'
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
shell: sh
|
|
run: |-
|
|
set -eu
|
|
|
|
# Remove virtual environment
|
|
rm -rf .venv
|
|
|
|
# Remove temporary files
|
|
rm -rf reports
|