# 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" < "$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