Files
actions/go-lint/action.yml
Ismo Vuorinen 7061aafd35 chore: add tests, update docs and actions (#299)
* docs: update documentation

* feat: validate-inputs has it's own pyproject

* security: mask DOCKERHUB_PASSWORD

* chore: add tokens, checkout, recrete docs, integration tests

* fix: add `statuses: write` permission to pr-lint
2025-10-18 13:09:19 +03:00

429 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: bash
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 -euo pipefail
# 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)
if [[ "$WORKING_DIRECTORY" == *".."* ]]; then
echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed"
exit 1
fi
# Validate golangci-lint version format
if [[ -n "$GOLANGCI_LINT_VERSION" ]] && [[ "$GOLANGCI_LINT_VERSION" != "latest" ]]; then
if ! [[ "$GOLANGCI_LINT_VERSION" =~ ^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 ! [[ "$GO_VERSION" =~ ^[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" ]] && [[ "$CONFIG_FILE" == *".."* ]]; then
echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed"
exit 1
fi
# Validate timeout format (duration with unit)
if ! [[ "$TIMEOUT" =~ ^[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() {
local value="$1"
local name="$2"
case "${value,,}" 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 ! [[ "$MAX_RETRIES" =~ ^[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() {
local linter_list="$1"
local name="$2"
if [[ -n "$linter_list" ]]; then
if ! [[ "$linter_list" =~ ^[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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
- name: Set up Cache
id: cache
if: inputs.cache == 'true'
uses: ./common-cache
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: bash
env:
MAX_RETRIES: ${{ inputs.max-retries }}
GOLANGCI_LINT_VERSION: ${{ inputs.golangci-lint-version }}
run: |
set -euo pipefail
# Function to install golangci-lint with retries
install_golangci_lint() {
local attempt=1
local max_attempts="$MAX_RETRIES"
local 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" ]] && [[ "$version" != v* ]]; then
install_version="v$version"
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: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CONFIG_FILE: ${{ inputs.config-file }}
TIMEOUT: ${{ inputs.timeout }}
run: |
set -euo pipefail
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: bash
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 -euo pipefail
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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
sarif_file: ${{ inputs.working-directory }}/reports/golangci-lint.sarif
category: golangci-lint
- 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 not being preserved
if [ "$CACHE" != "true" ]; then
rm -rf ~/.cache/golangci-lint
fi