From 8acb73eb07d00feda254b3a3cc1869861b82520e Mon Sep 17 00:00:00 2001 From: esengine <359807859@qq.com> Date: Sun, 9 Aug 2026 02:11:36 -0700 Subject: [PATCH] ci: run CI's linter locally, and stop timing out on slow Windows runners Two things cost a full CI round trip each, both avoidable. REASONIX.md told contributors golangci-lint was "not locally available", so nobody ran it. It always was: lint-cross already shells out to it. What was missing is a pinned version and one command. `.golangci-version` is now the single pin the Makefile and the lint job both read, `make lint-install` installs it (with CGO_ENABLED=0, so a stray clang on PATH cannot break the install), and `make lint` runs the same two gates CI runs, in the same order. Replaying a recent PR's diff through it reproduces the exact `modernize` finding CI took ten minutes to report. The Windows smoke step then failed on a run whose only change was two test fixtures. Across four consecutive runs of near-identical code, internal/boot measured 133s, 146s, 162s and 180s, and internal/agent 82s, 92s, 144s and 180s: a 2.2x spread from runner variance, four heavy packages sharing four vCPUs. A 3 minute per-package timeout sat inside that spread, so the step was one slow runner away from failing at any time. It is 8m now, still well inside the step's own 15 minute budget, which is what actually catches a hang. Neither change touches what is tested. --- .github/workflows/ci.yml | 18 +++++++++++++++--- .golangci-version | 1 + Makefile | 20 ++++++++++++++++++-- REASONIX.md | 7 +++++-- 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 .golangci-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c451ed62a8..3252278fac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,14 +138,21 @@ jobs: # filesystem contracts, plus cmd/. The full ./... sweep stays on pushes # to main-v2; the Unix legs always run the full suite. Keep the job-level # budget above normal 7-9 minute runner variance; each package test binary - # remains independently bounded by Go's 3 minute timeout below. + # remains independently bounded by Go's per-package timeout below. + # + # That timeout is 8m, not 3m: across four consecutive runs of the same + # code, internal/boot measured 133s, 146s, 162s and then hit 180s, and + # internal/agent 82s, 92s, 144s and 180s — a 2.2x spread from runner + # variance alone, with four heavy packages sharing four vCPUs. At 3m the + # step failed on slow runners without a code change; a hang still trips + # the 15 minute job budget above. - name: test (Windows smoke) if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request' timeout-minutes: 15 env: REASONIX_RELEASE_CACHE_GUARD: "1" WINDOWS_SANDBOX_WAIT_MS: "20000" - run: go test -p 4 -timeout=3m ./internal/agent/... ./internal/appidentity/... ./internal/boot/... ./internal/checkpoint/... ./internal/cli/... ./internal/control/... ./internal/desktoplauncher/... ./internal/filelock/... ./internal/fileutil/... ./internal/hook/... ./internal/instruction/... ./internal/mcplaunch/... ./internal/notify/... ./internal/proc/... ./internal/remote/... ./internal/repair/... ./internal/sandbox/... ./internal/sysproxy/... ./internal/workspacelease/... ./cmd/... + run: go test -p 4 -timeout=8m ./internal/agent/... ./internal/appidentity/... ./internal/boot/... ./internal/checkpoint/... ./internal/cli/... ./internal/control/... ./internal/desktoplauncher/... ./internal/filelock/... ./internal/fileutil/... ./internal/hook/... ./internal/instruction/... ./internal/mcplaunch/... ./internal/notify/... ./internal/proc/... ./internal/remote/... ./internal/repair/... ./internal/sandbox/... ./internal/sysproxy/... ./internal/workspacelease/... ./cmd/... # The general Windows PR smoke list intentionally omits internal/tool. # Keep the session-temp portability contract covered without widening the @@ -485,10 +492,15 @@ jobs: - name: repo standards run: go run ./tools/repolint + # `make lint-install` reads the same file, so a local run and this job + # cannot drift onto different linter versions. + - id: golangci + run: echo "version=$(cat .golangci-version)" >> "$GITHUB_OUTPUT" + - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.12.2 + version: ${{ steps.golangci.outputs.version }} args: --timeout=5m # The step above only type-checks the linux/amd64 build, so every diff --git a/.golangci-version b/.golangci-version new file mode 100644 index 0000000000..6f9cc441d9 --- /dev/null +++ b/.golangci-version @@ -0,0 +1 @@ +v2.12.2 diff --git a/Makefile b/Makefile index 3baf85ee31..1908482e1f 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,10 @@ LDFLAGS := -s -w \ -X main.gitCommit=$(GIT_COMMIT) \ -X main.buildTimeUTC=$(BUILD_TIME_UTC) GOEXE := $(shell go env GOEXE) +# One pin for the Makefile and the CI lint job; see .github/workflows/ci.yml. +GOLANGCI_VERSION := $(shell cat .golangci-version) -.PHONY: build vet fmt lint lint-cross lint-update test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean +.PHONY: build vet fmt lint lint-go lint-install lint-cross lint-update test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean build: CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/reasonix$(GOEXE) ./cmd/reasonix @@ -19,9 +21,23 @@ vet: fmt: gofmt -w . -lint: +# Both gates CI runs, at the version CI pins. Skipping golangci-lint locally +# trades a second here for a ten-minute CI round trip: `modernize` findings in +# particular never surface in `go vet`. +lint: lint-go go run ./tools/repolint +lint-go: + @command -v golangci-lint >/dev/null || { echo "golangci-lint not installed; run: make lint-install"; exit 1; } + @have=$$(golangci-lint version --short 2>/dev/null); want=$$(echo "$(GOLANGCI_VERSION)" | sed 's/^v//'); \ + [ "$$have" = "$$want" ] || echo "warning: local golangci-lint $$have, CI pins $$want (make lint-install)" + golangci-lint run --timeout=5m ./... + +# CGO_ENABLED=0 keeps the install working where a stray clang on PATH shadows +# the toolchain and breaks runtime/cgo. +lint-install: + CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) + lint-update: go run ./tools/repolint -update diff --git a/REASONIX.md b/REASONIX.md index ef68d3287e..408c80526c 100644 --- a/REASONIX.md +++ b/REASONIX.md @@ -66,11 +66,14 @@ Run these **before every commit** to catch the fastest CI failures locally: ```bash gofmt -w . # catches gofmt (saves ~13s CI) go vet ./... # catches vet warnings (saves ~52s CI/lint) -go run ./tools/repolint # catches comment/size/layering regressions +make lint # golangci-lint at CI's pin + repolint go test ./internal/tool/builtin/ ./internal/boot/ # catches tool/boot test breaks ``` -CI runs `golangci-lint` (not locally available), but gofmt + vet already block ~80% of fast-fail scenarios. +`make lint` runs both gates CI runs, at the version in `.golangci-version`; +`make lint-install` installs it. Do not skip it: a `modernize` finding never +shows up in `go vet`, and the CI round trip that catches it instead costs ten +minutes. ## Import cycle rule