From 90d6283168006c558fdcc663a8ea520ed8b366b3 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 12 Jun 2026 11:18:42 -0700 Subject: [PATCH 1/4] ci: add `make verify` + pin golangci-lint for local CI parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the "passes locally, fails CI" gap: the dev loop ran go build / test / vet but not golangci-lint, so staticcheck violations (SA9005, SA4015, ST1023) slipped past local checks and failed CI. - `make verify`: runs the exact CI gate in order — lint, test, check-bindings. One command for local CI parity; run before pushing. - Pin golangci-lint to v2.12.2 (what the workflow's `latest` currently resolves to) in three synced places: GOLANGCI_VERSION (Makefile), the golangci-lint-action `version:`, and a new `.golangci.yml`. - `install-lint` target (go install at the pinned tag), wired into install-tools. `make lint` warns if the PATH binary differs. - `.golangci.yml` pins v2.12.2's default linter set explicitly (errcheck, govet, ineffassign, staticcheck, unused) for determinism, not stricter-than-today — repo is clean under it. - Document the pre-push flow in `make help` and README. `make verify` passes end-to-end and `golangci-lint run` is clean on this branch under v2.12.2. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-and-test.yml | 6 +++- .golangci.yml | 23 +++++++++++++ Makefile | 49 +++++++++++++++++++++++++--- README.md | 22 ++++++++++++- 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6716eb1..3f2689b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -42,7 +42,11 @@ jobs: - name: Install golangci-lint uses: golangci/golangci-lint-action@v7 with: - version: latest + # Pinned (not `latest`) for determinism. MUST match GOLANGCI_VERSION in + # the Makefile and the `version:` in .golangci.yml's schema. `latest` + # drifts between releases and shifts the default linter set, which is a + # "passes locally, fails CI" trap. Bump all three together. + version: v2.12.2 - name: Run linting run: make lint diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8978ddf --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,23 @@ +# golangci-lint configuration (schema v2). +# +# Purpose: DETERMINISM, not stricter-than-today. golangci-lint's built-in +# default linter set shifts between releases; pinning it here (alongside a +# pinned binary version in the Makefile + CI) guarantees the same checks run +# locally and in CI. This list is exactly golangci-lint v2.12.2's +# enabled-by-default set, encoded explicitly so a future binary bump can't +# silently add/remove linters under us. +# +# Keep in sync with GOLANGCI_VERSION in the Makefile. +version: "2" + +linters: + # `default: none` + an explicit list pins the enabled set. The five linters + # below are v2.12.2's defaults; staticcheck is the one that caught the + # SA9005/SA4015/ST1023 violations that slipped past local `go vet`. + default: none + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - unused diff --git a/Makefile b/Makefile index 9c52625..4536d53 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,14 @@ SOLC_EVM_VERSION := paris # Falls back to grepping go.mod if `go list` is unavailable. GETH_VERSION := $(shell go list -m -f '{{.Version}}' github.com/ethereum/go-ethereum 2>/dev/null || grep -E 'github.com/ethereum/go-ethereum ' go.mod | awk '{print $$2}') +# Pinned golangci-lint version. MUST match the `version:` pinned in +# .github/workflows/build-and-test.yml (golangci-lint-action). golangci-lint's +# default linter set + check behavior drifts between releases, so an unpinned +# `latest` is a "passes locally, fails CI" trap. `make install-lint` installs +# exactly this version; `make lint` warns if the binary on PATH differs. +# Bump here + in the workflow + re-validate `.golangci.yml` together. +GOLANGCI_VERSION := 2.12.2 + # Find all .sol files in contracts directory SOL_FILES := $(wildcard $(CONTRACTS_DIR)/*.sol) CONTRACT_NAMES := $(basename $(notdir $(SOL_FILES))) @@ -52,23 +60,29 @@ BIN_FILES := $(addprefix $(BUILD_DIR)/, $(addsuffix .bin, $(CONTRACT_NAMES))) BINDING_FILES := $(addprefix $(BINDINGS_DIR)/, $(addsuffix .go, $(CONTRACT_NAMES))) SCENARIO_TEMPLATE_FILES := $(addprefix $(SCENARIOS_DIR)/, $(addsuffix .go, $(CONTRACT_NAMES))) -.PHONY: generate generate-bindings check-bindings install-abigen clean help build-cli install setup-node build test lint +.PHONY: generate generate-bindings check-bindings install-abigen install-lint clean help build-cli install setup-node build test lint verify # Default target help: @echo "Available targets:" + @echo " verify - Run exactly what CI gates on: lint + test + check-bindings" @echo " build - Build the seiload CLI (alias for build-cli)" - @echo " test - Run tests with coverage" - @echo " lint - Run linting and static analysis" + @echo " test - Run tests with coverage (race detector enabled)" + @echo " lint - Run linting and static analysis (golangci-lint $(GOLANGCI_VERSION))" @echo " setup-node - Install nvm, Node.js 20, and solc" @echo " generate - Generate Go bindings and scenario templates for all contracts" @echo " generate-bindings - Regenerate ONLY the Go bindings (no scenarios/factory)" @echo " check-bindings - Fail if committed bindings are out of sync with contracts" + @echo " install-tools - Install the full pinned toolchain (solc, abigen, golangci-lint)" @echo " install-abigen - Install abigen pinned to the go.mod go-ethereum version" + @echo " install-lint - Install golangci-lint pinned to $(GOLANGCI_VERSION)" @echo " clean - Remove generated files" @echo " help - Show this help message" @echo " build-cli - Build the seiload CLI" @echo " install - Install the seiload CLI" + @echo "" + @echo "Before pushing: run 'make verify' (local CI parity). Run 'make install-tools'" + @echo "first to get the pinned toolchain (golangci-lint $(GOLANGCI_VERSION) etc.)." # Setup Node.js environment with nvm setup-node: @@ -187,8 +201,17 @@ check-bindings: fi @echo "✅ Bindings are in sync with contracts" +# Install golangci-lint pinned to GOLANGCI_VERSION (see note above the variable). +# `go install` at the exact tag keeps the linter binary — and therefore the +# default linter set / check behavior — reproducible across dev machines and CI. +# CI installs the same version via golangci-lint-action (pinned, not `latest`). +install-lint: + @echo "📦 Installing golangci-lint@v$(GOLANGCI_VERSION) ..." + @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$(GOLANGCI_VERSION) + @echo "✅ Installed golangci-lint@v$(GOLANGCI_VERSION)" + # Install tools (optional convenience target) -install-tools: setup-node install-abigen +install-tools: setup-node install-abigen install-lint @echo "✅ Tools installation complete" # Build the seiload CLI binary @@ -215,8 +238,24 @@ test: @go tool cover -func=coverage.out @echo "✅ Tests passed" -# Run linting and static analysis +# Run linting and static analysis. +# Expects golangci-lint pinned to GOLANGCI_VERSION (run `make install-lint`). +# We warn — not fail — on a version mismatch: the linter set is pinned in +# .golangci.yml, but a different binary can still shift check results, which is +# exactly the "passes locally, fails CI" trap this target guards against. lint: @echo "🔍 Running linting and static analysis..." + @have=$$(golangci-lint version --short 2>/dev/null || golangci-lint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ -n "$$have" ] && [ "$$have" != "$(GOLANGCI_VERSION)" ]; then \ + echo "⚠️ golangci-lint $$have on PATH != pinned $(GOLANGCI_VERSION). Run 'make install-lint' for CI parity."; \ + fi @golangci-lint run @echo "✅ Linting and static analysis passed" + +# Local CI parity: run exactly what CI gates on, in the same order. +# - lint -> .github/workflows/build-and-test.yml (make lint) +# - test -> .github/workflows/build-and-test.yml (make test) +# - check-bindings -> .github/workflows/bindings-check.yml (make check-bindings) +# Run this before pushing: a green `make verify` means the gating CI jobs pass. +verify: lint test check-bindings + @echo "✅ verify passed (lint + test + check-bindings) — local CI parity" diff --git a/README.md b/README.md index 2a6f645..c09b3ea 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,26 @@ blocks height=5191 time(p50=2s p99=5s max=8s) gas(p50=21000 p99=50000 max=100000 ## Development +### Before you push + +Run the full local CI gate in one command: + +```bash +make verify # lint + test + check-bindings (exactly what CI gates on) +``` + +A green `make verify` means the gating CI jobs (`build-and-test`, `bindings-check`) +will pass. Install the pinned toolchain once first so your local results match CI: + +```bash +make install-tools # solc, abigen, and golangci-lint (pinned to v2.12.2) +``` + +`golangci-lint` is pinned to a specific version (Makefile `GOLANGCI_VERSION`, +the workflow's `golangci-lint-action` `version:`, and `.golangci.yml`); `make lint` +warns if the binary on your PATH differs. A drifting/unpinned linter is the usual +"passes locally, fails CI" trap — `make install-lint` gives you the exact CI version. + ### Build ```bash make build @@ -177,7 +197,7 @@ make build ### Test ```bash -make test +make test # runs with -race ``` ### Lint From 2015776a57cb452ec54773cf1c64daa1546f43af Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 12 Jun 2026 11:21:52 -0700 Subject: [PATCH 2/4] ci: clarify .golangci.yml comment + install-tools docs (review nits) - Note staticcheck subsumes ST* stylecheck diagnostics in v2 (ST1023). - install-tools also sets up Node via nvm; point to install-lint for linter-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- .golangci.yml | 5 +++-- README.md | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 8978ddf..9368285 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,8 +12,9 @@ version: "2" linters: # `default: none` + an explicit list pins the enabled set. The five linters - # below are v2.12.2's defaults; staticcheck is the one that caught the - # SA9005/SA4015/ST1023 violations that slipped past local `go vet`. + # below are v2.12.2's defaults; staticcheck (which in v2 also runs the ST* + # stylecheck diagnostics) is the one that caught the SA9005/SA4015 and ST1023 + # violations that slipped past local `go vet`. default: none enable: - errcheck diff --git a/README.md b/README.md index c09b3ea..9ef5b7b 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,9 @@ A green `make verify` means the gating CI jobs (`build-and-test`, `bindings-chec will pass. Install the pinned toolchain once first so your local results match CI: ```bash -make install-tools # solc, abigen, and golangci-lint (pinned to v2.12.2) +make install-tools # full toolchain: Node (via nvm), solc, abigen, golangci-lint (pinned to v2.12.2) +# or, for the linter only: +make install-lint # golangci-lint pinned to v2.12.2 ``` `golangci-lint` is pinned to a specific version (Makefile `GOLANGCI_VERSION`, From 00836f68a765b0e298229e792278cf4747340dd6 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 12 Jun 2026 14:04:03 -0700 Subject: [PATCH 3/4] ci: lean .golangci.yml header to critical-why + README pointer The header restated the README "Before you push" pinning narrative and the inline default:none comment re-listed the linters plus a one-off incident history. Keep the load-bearing why (determinism, pinned v2.12.2 default set, keep-in-sync with GOLANGCI_VERSION); point to the README for the rest. Co-Authored-By: Claude Opus 4.8 (1M context) --- .golangci.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9368285..b1d7811 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,20 +1,13 @@ # golangci-lint configuration (schema v2). # -# Purpose: DETERMINISM, not stricter-than-today. golangci-lint's built-in -# default linter set shifts between releases; pinning it here (alongside a -# pinned binary version in the Makefile + CI) guarantees the same checks run -# locally and in CI. This list is exactly golangci-lint v2.12.2's -# enabled-by-default set, encoded explicitly so a future binary bump can't -# silently add/remove linters under us. -# -# Keep in sync with GOLANGCI_VERSION in the Makefile. +# DETERMINISM, not stricter-than-today: this is exactly golangci-lint v2.12.2's +# enabled-by-default linter set, encoded explicitly so a binary bump can't +# silently shift it. Keep in sync with GOLANGCI_VERSION in the Makefile. +# See README "Before you push" for the full pinning/parity rationale. version: "2" linters: - # `default: none` + an explicit list pins the enabled set. The five linters - # below are v2.12.2's defaults; staticcheck (which in v2 also runs the ST* - # stylecheck diagnostics) is the one that caught the SA9005/SA4015 and ST1023 - # violations that slipped past local `go vet`. + # `default: none` + the explicit list below pins the enabled set. default: none enable: - errcheck From 58e0dfc17f5dda9fa072d6a5d732adf74916ee42 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 12 Jun 2026 14:53:28 -0700 Subject: [PATCH 4/4] ci: lean Makefile comments to critical-only (comment sweep, #49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trim the new-target comments (GOLANGCI_VERSION, install-lint, lint, verify) to the critical at-site context — the 3-way version sync coupling, the warn-not-fail rationale, what verify runs — leaning on the README 'Before you push' section for the full pinning/parity narrative. Comment-only; no target behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 4536d53..4e8da85 100644 --- a/Makefile +++ b/Makefile @@ -42,12 +42,9 @@ SOLC_EVM_VERSION := paris # Falls back to grepping go.mod if `go list` is unavailable. GETH_VERSION := $(shell go list -m -f '{{.Version}}' github.com/ethereum/go-ethereum 2>/dev/null || grep -E 'github.com/ethereum/go-ethereum ' go.mod | awk '{print $$2}') -# Pinned golangci-lint version. MUST match the `version:` pinned in -# .github/workflows/build-and-test.yml (golangci-lint-action). golangci-lint's -# default linter set + check behavior drifts between releases, so an unpinned -# `latest` is a "passes locally, fails CI" trap. `make install-lint` installs -# exactly this version; `make lint` warns if the binary on PATH differs. -# Bump here + in the workflow + re-validate `.golangci.yml` together. +# Pinned golangci-lint version. Keep in sync with the workflow `version:` and +# `.golangci.yml` (bump all three together); an unpinned `latest` drifts into a +# "passes locally, fails CI" trap. See README "Before you push". GOLANGCI_VERSION := 2.12.2 # Find all .sol files in contracts directory @@ -201,10 +198,8 @@ check-bindings: fi @echo "✅ Bindings are in sync with contracts" -# Install golangci-lint pinned to GOLANGCI_VERSION (see note above the variable). -# `go install` at the exact tag keeps the linter binary — and therefore the -# default linter set / check behavior — reproducible across dev machines and CI. -# CI installs the same version via golangci-lint-action (pinned, not `latest`). +# Install golangci-lint pinned to GOLANGCI_VERSION for CI parity (CI pins the +# same version via golangci-lint-action). install-lint: @echo "📦 Installing golangci-lint@v$(GOLANGCI_VERSION) ..." @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$(GOLANGCI_VERSION) @@ -238,11 +233,8 @@ test: @go tool cover -func=coverage.out @echo "✅ Tests passed" -# Run linting and static analysis. -# Expects golangci-lint pinned to GOLANGCI_VERSION (run `make install-lint`). -# We warn — not fail — on a version mismatch: the linter set is pinned in -# .golangci.yml, but a different binary can still shift check results, which is -# exactly the "passes locally, fails CI" trap this target guards against. +# Run linting. Expects golangci-lint == GOLANGCI_VERSION (`make install-lint`); +# warns (not fails) on a mismatch, since a different binary can shift results. lint: @echo "🔍 Running linting and static analysis..." @have=$$(golangci-lint version --short 2>/dev/null || golangci-lint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ @@ -252,10 +244,7 @@ lint: @golangci-lint run @echo "✅ Linting and static analysis passed" -# Local CI parity: run exactly what CI gates on, in the same order. -# - lint -> .github/workflows/build-and-test.yml (make lint) -# - test -> .github/workflows/build-and-test.yml (make test) -# - check-bindings -> .github/workflows/bindings-check.yml (make check-bindings) -# Run this before pushing: a green `make verify` means the gating CI jobs pass. +# Local CI parity: lint + test + check-bindings — the gating jobs in +# build-and-test.yml + bindings-check.yml. Green = those CI jobs pass. verify: lint test check-bindings @echo "✅ verify passed (lint + test + check-bindings) — local CI parity"