Skip to content

lint-go.yml

Formatting, go vet and optionally golangci-lint over a Go module. Detects the module first and reports rather than passes when there is not one.

go vet runs once per build tag set, because a build tag produces a second binary and code that only compiles under one of them is otherwise only vetted under one.

Inputs

InputTypeDescriptionRequiredDefault
GO_VERSIONstringGo version to use. Empty reads it from go.modNo""
WORKING_DIRECTORYstringWorking directory for the module to lintNo"."
PACKAGESstringPackage pattern to vetNo"./..."
BUILD_TAGSstringBuild tag sets as a JSON array of strings; one go vet pass eachNo'[""]'
FORMATbooleanWhether to check formatting with gofmtNotrue
FORMAT_PATHSstringPaths to check formatting in, space separatedNo"."
VETbooleanWhether to run go vetNotrue
GOLANGCI_LINTstring"true", "false", or empty to run it when a .golangci configuration file is presentNo""
GOLANGCI_LINT_VERSIONstringgolangci-lint version to installNo"v2.6.2"
GOLANGCI_LINT_ARGSstringExtra arguments for golangci-lint runNo""
CACHEbooleanWhether to cache the module and build cachesNotrue
RUNS_ONstringRunner labels as JSON arrayNo'["ubuntu-24.04"]'
FAIL_ON_ERRORbooleanWhether to fail the workflow on lint findingsNotrue

Permissions

ScopeAccessDescription
contentsreadRead source files

Notes

  • gofmt -l exits 0 whether or not it found anything, naming the files it would rewrite on stdout. The emptiness of that output is the result, so this workflow tests the output rather than the status — a step that forwarded gofmt's exit code would pass unconditionally and look exactly like a clean tree.
  • golangci-lint is opt-in by configuration. Empty GOLANGCI_LINT runs it when a .golangci.yml, .yaml, .toml or .json is present: enabling it by default would hold every caller to a linter they never chose, and defaulting it off would ignore a configuration file somebody wrote on purpose. An explicit "true" or "false" wins over the file in both directions.
  • The vet passes share one job rather than a matrix. Vet is seconds, and a matrix would pay for a checkout and a toolchain per pass to parallelise something shorter than its own setup. A finding in any pass fails the step; a later clean pass cannot clear an earlier one.
  • The Go version comes from go.mod unless GO_VERSION says otherwise.
  • The golangci-lint action's own cache is disabled, because setup-go has already restored one and the two disagree about what is current often enough to be worth having only one.

Usage

Basic

yaml
jobs:
  lint:
    uses: this-is-tobi/github-workflows/.github/workflows/lint-go.yml@v0
    permissions:
      contents: read

Narrowing the formatting check

Worth doing where a repository holds other modules that are formatted on their own — plugins, examples, generated trees.

yaml
jobs:
  lint:
    uses: this-is-tobi/github-workflows/.github/workflows/lint-go.yml@v0
    permissions:
      contents: read
    with:
      FORMAT_PATHS: "./builtin ./cmd ./internal ./pkg"

Vetting under a build tag

yaml
jobs:
  lint:
    uses: this-is-tobi/github-workflows/.github/workflows/lint-go.yml@v0
    permissions:
      contents: read
    with:
      BUILD_TAGS: '["", "ai"]'

With golangci-lint

Nothing needs saying when a .golangci.yml is committed — it is picked up. Pass the input to run it without one, or to keep it off in a repository that has one.

yaml
jobs:
  lint:
    uses: this-is-tobi/github-workflows/.github/workflows/lint-go.yml@v0
    permissions:
      contents: read
    with:
      GOLANGCI_LINT: "true"
      GOLANGCI_LINT_ARGS: "--timeout 5m"