# 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@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.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@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 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" < "$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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 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