mirror of
https://github.com/ivuorinen/actions.git
synced 2026-03-02 17:53:57 +00:00
* fix(deps): replace step-security/retry with nick-fields/retry * chore(deps): update github action sha pins via pinact * refactor: remove common-retry references from tests and validators * chore: simplify description fallback and update action count * docs: remove hardcoded test counts from memory and docs Replace exact "769 tests" references with qualitative language so these files don't go stale as test count grows.
440 lines
14 KiB
YAML
440 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@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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: Cache golangci-lint
|
|
id: cache
|
|
if: inputs.cache == 'true'
|
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
|
with:
|
|
path: |
|
|
~/.cache/golangci-lint
|
|
~/.cache/go-build
|
|
key: ${{ runner.os }}-golangci-${{ inputs.golangci-lint-version }}-${{ hashFiles('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@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
|
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
|