Files
actions/go-lint/action.yml

439 lines
14 KiB
YAML

# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
# permissions:
# - contents: read # Required for checking out repository
# - security-events: write # Required for uploading SARIF results
---
name: Go Lint Check
description: 'Run golangci-lint with advanced configuration, caching, and reporting'
author: Ismo Vuorinen
branding:
icon: code
color: blue
inputs:
working-directory:
description: 'Directory containing Go files'
required: false
default: '.'
golangci-lint-version:
description: 'Version of golangci-lint to use'
required: false
default: 'latest'
go-version:
description: 'Go version to use'
required: false
default: 'stable'
config-file:
description: 'Path to golangci-lint config file'
required: false
default: '.golangci.yml'
timeout:
description: 'Timeout for analysis (e.g., 5m, 1h)'
required: false
default: '5m'
cache:
description: 'Enable golangci-lint 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, github-actions)'
required: false
default: 'sarif'
max-retries:
description: 'Maximum number of retry attempts'
required: false
default: '3'
only-new-issues:
description: 'Report only new issues since main branch'
required: false
default: 'true'
disable-all:
description: 'Disable all linters (useful with --enable-*)'
required: false
default: 'false'
enable-linters:
description: 'Comma-separated list of linters to enable'
required: false
disable-linters:
description: 'Comma-separated list of linters to disable'
required: false
token:
description: 'GitHub token for authentication'
required: false
default: ''
outputs:
error-count:
description: 'Number of errors found'
value: ${{ steps.lint.outputs.error_count }}
sarif-file:
description: 'Path to SARIF report file'
value: ${{ steps.lint.outputs.sarif_file }}
cache-hit:
description: 'Indicates if there was a cache hit'
value: ${{ steps.cache.outputs.cache-hit }}
analyzed-files:
description: 'Number of files analyzed'
value: ${{ steps.lint.outputs.analyzed_files }}
runs:
using: composite
steps:
- name: Validate Inputs
id: validate
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
GOLANGCI_LINT_VERSION: ${{ inputs.golangci-lint-version }}
GO_VERSION: ${{ inputs.go-version }}
CONFIG_FILE: ${{ inputs.config-file }}
TIMEOUT: ${{ inputs.timeout }}
CACHE: ${{ inputs.cache }}
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
ONLY_NEW_ISSUES: ${{ inputs.only-new-issues }}
DISABLE_ALL: ${{ inputs.disable-all }}
REPORT_FORMAT: ${{ inputs.report-format }}
MAX_RETRIES: ${{ inputs.max-retries }}
ENABLE_LINTERS: ${{ inputs.enable-linters }}
DISABLE_LINTERS: ${{ inputs.disable-linters }}
run: |
set -eu
# Validate working directory exists
if [ ! -d "$WORKING_DIRECTORY" ]; then
echo "::error::Working directory not found at '$WORKING_DIRECTORY'"
exit 1
fi
# Validate working directory path security (prevent traversal)
case "$WORKING_DIRECTORY" in
*..*)
echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed"
exit 1
;;
esac
# Validate golangci-lint version format
if [ -n "$GOLANGCI_LINT_VERSION" ] && [ "$GOLANGCI_LINT_VERSION" != "latest" ]; then
if ! echo "$GOLANGCI_LINT_VERSION" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$'; then
echo "::error::Invalid golangci-lint-version format: '$GOLANGCI_LINT_VERSION'. Expected format: vX.Y.Z or 'latest' (e.g., v1.55.2, latest)"
exit 1
fi
fi
# Validate Go version format
if [ -n "$GO_VERSION" ] && [ "$GO_VERSION" != "stable" ]; then
if ! echo "$GO_VERSION" | grep -Eq '^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$'; then
echo "::error::Invalid go-version format: '$GO_VERSION'. Expected format: X.Y or X.Y.Z or 'stable' (e.g., 1.21, 1.21.5, stable)"
exit 1
fi
fi
# Validate config file path if not default
if [ "$CONFIG_FILE" != ".golangci.yml" ]; then
case "$CONFIG_FILE" in
*..*)
echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed"
exit 1
;;
esac
fi
# Validate timeout format (duration with unit)
if ! echo "$TIMEOUT" | grep -Eq '^[0-9]+(ns|us|µs|ms|s|m|h)$'; then
echo "::error::Invalid timeout format: '$TIMEOUT'. Expected format with unit: 5m, 1h, 300s (e.g., 5m, 30s, 2h)"
exit 1
fi
# Validate boolean inputs
validate_boolean() {
_value="$1"
_name="$2"
_value_lower=$(echo "$_value" | tr '[:upper:]' '[:lower:]')
case "$_value_lower" in
true|false)
;;
*)
echo "::error::Invalid boolean value for $_name: '$_value'. Expected: true or false"
exit 1
;;
esac
}
validate_boolean "$CACHE" "cache"
validate_boolean "$FAIL_ON_ERROR" "fail-on-error"
validate_boolean "$ONLY_NEW_ISSUES" "only-new-issues"
validate_boolean "$DISABLE_ALL" "disable-all"
# Validate report format enumerated values
case "$REPORT_FORMAT" in
checkstyle|colored-line-number|github-actions|html|json|junit-xml|line-number|sarif|tab|teamcity|xml)
;;
*)
echo "::error::Invalid report-format: '$REPORT_FORMAT'. Allowed values:" \
"checkstyle, colored-line-number, github-actions, html, json, junit-xml, line-number, sarif, tab, teamcity, xml"
exit 1
;;
esac
# Validate max retries (positive integer with reasonable upper limit)
if ! echo "$MAX_RETRIES" | grep -Eq '^[0-9]+$' || [ "$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 linter lists if provided
validate_linter_list() {
_linter_list="$1"
_name="$2"
if [ -n "$_linter_list" ]; then
if ! echo "$_linter_list" | grep -Eq '^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$'; then
echo "::error::Invalid $_name format: '$_linter_list'. Expected comma-separated linter names (e.g., gosec,govet,staticcheck)"
exit 1
fi
fi
}
validate_linter_list "$ENABLE_LINTERS" "enable-linters"
validate_linter_list "$DISABLE_LINTERS" "disable-linters"
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ inputs.go-version }}
cache: true
- name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
with:
token: ${{ inputs.token || github.token }}
- name: Set up Cache
id: cache
if: inputs.cache == 'true'
uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42
with:
type: 'go'
paths: '~/.cache/golangci-lint,~/.cache/go-build'
key-prefix: 'golangci-${{ inputs.golangci-lint-version }}'
key-files: 'go.sum,${{ inputs.config-file }}'
restore-keys: '${{ runner.os }}-golangci-${{ inputs.golangci-lint-version }}-'
- name: Install golangci-lint
shell: sh
env:
MAX_RETRIES: ${{ inputs.max-retries }}
GOLANGCI_LINT_VERSION: ${{ inputs.golangci-lint-version }}
run: |
set -eu
# Function to install golangci-lint with retries
install_golangci_lint() {
_attempt=1
_max_attempts="$MAX_RETRIES"
_version="$GOLANGCI_LINT_VERSION"
while [ $_attempt -le $_max_attempts ]; do
echo "Installation attempt $_attempt of $_max_attempts"
# Add 'v' prefix if version is not 'latest' and doesn't already have it
install_version="$_version"
if [ "$_version" != "latest" ]; then
case "$_version" in
v*) ;;
*) install_version="v$_version" ;;
esac
fi
if curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b "$(go env GOPATH)/bin" "$install_version"; then
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 golangci-lint after $_max_attempts attempts"
return 1
}
install_golangci_lint
- name: Prepare Configuration
id: config
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CONFIG_FILE: ${{ inputs.config-file }}
TIMEOUT: ${{ inputs.timeout }}
run: |
set -eu
cd "$WORKING_DIRECTORY"
# Create default config if none exists
if [ ! -f "$CONFIG_FILE" ]; then
echo "Creating default golangci-lint configuration..."
cat > "$CONFIG_FILE" <<EOF
linters:
enable-all: true
disable:
- exhaustivestruct
- interfacer
- scopelint
- maligned
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0.8
gocyclo:
min-complexity: 15
dupl:
threshold: 100
goconst:
min-len: 3
min-occurrences: 3
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
new: true
run:
deadline: $TIMEOUT
tests: true
skip-dirs:
- vendor
- third_party
EOF
fi
- name: Run golangci-lint
id: lint
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
DISABLE_ALL: ${{ inputs.disable-all }}
ENABLE_LINTERS: ${{ inputs.enable-linters }}
DISABLE_LINTERS: ${{ inputs.disable-linters }}
CONFIG_FILE: ${{ inputs.config-file }}
TIMEOUT: ${{ inputs.timeout }}
CACHE: ${{ inputs.cache }}
ONLY_NEW_ISSUES: ${{ inputs.only-new-issues }}
REPORT_FORMAT: ${{ inputs.report-format }}
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
run: |
set -eu
cd "$WORKING_DIRECTORY"
# Create reports directory
mkdir -p reports
# Prepare linter configuration
linter_args=""
if [ "$DISABLE_ALL" = "true" ]; then
linter_args="--disable-all"
fi
if [ -n "$ENABLE_LINTERS" ]; then
linter_args="$linter_args --enable=$ENABLE_LINTERS"
fi
if [ -n "$DISABLE_LINTERS" ]; then
linter_args="$linter_args --disable=$DISABLE_LINTERS"
fi
# Prepare cache flag
cache_flag="--no-cache"
if [ "$CACHE" = "true" ]; then
cache_flag="--cache"
fi
# Prepare new issues flag
new_flag=""
if [ "$ONLY_NEW_ISSUES" = "true" ]; then
new_flag="--new"
fi
# Run golangci-lint
echo "Running golangci-lint..."
result_file="reports/golangci-lint.$REPORT_FORMAT"
GOLANGCI_LINT_CACHE="$HOME/.cache/golangci-lint" \
golangci-lint run \
--config "$CONFIG_FILE" \
--timeout "$TIMEOUT" \
$cache_flag \
$new_flag \
--out-format "$REPORT_FORMAT" \
$linter_args \
./... > "$result_file" || {
exit_code=$?
# Count errors
if [ "$REPORT_FORMAT" = "json" ]; then
if command -v jq >/dev/null 2>&1; then
error_count=$(jq '.Issues | length' "$result_file" 2>/dev/null || echo 0)
else
echo "::warning::jq not found - falling back to grep for error counting"
error_count=$(grep -c '"level": "error"' "$result_file" 2>/dev/null || echo 0)
fi
else
error_count=$(grep -c "level\": \"error\"" "$result_file" || echo 0)
fi
echo "error_count=${error_count}" >> $GITHUB_OUTPUT
if [ "$FAIL_ON_ERROR" = "true" ]; then
echo "::error::golangci-lint found ${error_count} issues"
exit $exit_code
fi
}
# Count analyzed files
analyzed_files=$(find . -type f -name "*.go" -not -path "./vendor/*" -not -path "./.git/*" | wc -l)
echo "analyzed_files=${analyzed_files}" >> $GITHUB_OUTPUT
echo "sarif_file=$result_file" >> $GITHUB_OUTPUT
- name: Upload Lint Results
if: always() && inputs.report-format == 'sarif'
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
with:
sarif_file: ${{ inputs.working-directory }}/reports/golangci-lint.sarif
category: golangci-lint
- name: Cleanup
if: always()
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CACHE: ${{ inputs.cache }}
run: |-
set -eu
cd "$WORKING_DIRECTORY"
# Remove temporary files
rm -rf reports/
# Clean cache if not being preserved
if [ "$CACHE" != "true" ]; then
rm -rf ~/.cache/golangci-lint
fi