From 4663474849a82f60c9f869546bd524702e95eb7a Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Sat, 5 Sep 2026 10:46:01 -0500 Subject: [PATCH] feat(go-ci): warm the build cache before linting, and let callers set the timeout golangci-lint typechecks the whole module while loading packages. With nothing in GOCACHE that can outrun the deadline before a single linter runs, and the failure names neither the repo nor the cause: context loading failed: failed to load packages: context deadline exceeded foundry hits this on every run. twinos already carries the same fix inline, so this moves it where the other consumers get it too. The lint job now builds the module first, which fills GOCACHE so package loading reuses the compiled output. The step is non-fatal on purpose: a module that genuinely does not compile deserves a golangci-lint report with a file and a line, not a bare build failure in a step the caller never asked for. golangci-lint-timeout exposes the deadline, still 5m by default. Warming the cache addresses the usual cause, so this is for repos where linting itself is honestly slow rather than a way to paper over a cold start. --- .github/workflows/go-ci.yml | 21 ++++++++++++++++++++- README.md | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 08229c8..65a5f19 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -22,6 +22,10 @@ on: golangci-lint-version: type: string default: 'v2.12.2' + golangci-lint-timeout: + description: 'Timeout passed to golangci-lint. The job warms the build cache first, which is what usually blows the deadline on a large module, so raise this only if linting itself is genuinely slow.' + type: string + default: '5m' gosec-version: type: string default: 'v2.28.0' @@ -326,12 +330,27 @@ jobs: check-latest: true cache-dependency-path: ${{ inputs.working-directory }}/go.mod + # Warm the build cache before linting. On a cold cache golangci-lint has to + # typecheck the entire module while loading packages, and on a large repo + # that alone blows the deadline: "context loading failed: context deadline + # exceeded". Building first fills GOCACHE so loading reuses the compiled + # output. + # + # Non-fatal on purpose. A module that genuinely does not compile should be + # reported by golangci-lint, with file and line, and not as a bare build + # failure in a step the caller never asked for. + - name: Warm build cache + continue-on-error: true + shell: bash + working-directory: ${{ inputs.working-directory }} + run: go build ./... + - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: version: ${{ inputs.golangci-lint-version }} working-directory: ${{ inputs.working-directory }} - args: --timeout=5m + args: --timeout=${{ inputs.golangci-lint-timeout }} verify: name: Verify diff --git a/README.md b/README.md index c5c330e..d521ea2 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,28 @@ a monorepo, while its CI deliberately tests only the root module, would get a much broader (and slower) run than intended. Pass `prefer-makefile: false` to force the plain `go` commands. +## Lint on a cold cache + +golangci-lint typechecks the whole module while it loads packages. On a big +repo, with nothing in GOCACHE, that alone can outrun the deadline before a +single linter runs: + +``` +context loading failed: failed to load packages: context deadline exceeded +``` + +The lint job builds the module first so GOCACHE is populated and loading reuses +the compiled output. That step is deliberately non-fatal: a module that truly +does not compile should be reported by golangci-lint with a file and a line, not +as a bare build failure in a step you never asked for. + +If linting is still slow once the cache is warm, raise the deadline: + +```yaml +with: + golangci-lint-timeout: '15m' +``` + ## Splitting gating from non-gating `only-test: true` runs just the test matrix, skipping lint, verify and