From 90d6283168006c558fdcc663a8ea520ed8b366b3 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Fri, 12 Jun 2026 11:18:42 -0700 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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" From 1ccedeea985073f31bdbbb46f7910bfb0670bf3c Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 15 Jun 2026 09:47:15 -0700 Subject: [PATCH 5/8] ci: add build + --help smoke to `make verify` for true CI parity (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify ran lint/test/check-bindings but build-and-test.yml also runs `make build`, `./build/seiload --help`, and a dry-run smoke. A main/CLI that failed to compile could pass verify yet fail CI. Fold build + --help into verify (cheap, deterministic). Keep CI's dry-run smoke CI-only — it's a backgrounded run killed after 5s with no exit-code assertion, not worth the wall-time tax — and make the README/Makefile wording state exactly what verify covers vs. what CI adds. Co-Authored-By: Claude Opus 4.8 --- Makefile | 16 +++++++++++----- README.md | 10 +++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 4e8da85..e302e67 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ SCENARIO_TEMPLATE_FILES := $(addprefix $(SCENARIOS_DIR)/, $(addsuffix .go, $(CON # Default target help: @echo "Available targets:" - @echo " verify - Run exactly what CI gates on: lint + test + check-bindings" + @echo " verify - Run the gating CI checks: lint + test + build + CLI --help + check-bindings" @echo " build - Build the seiload CLI (alias for build-cli)" @echo " test - Run tests with coverage (race detector enabled)" @echo " lint - Run linting and static analysis (golangci-lint $(GOLANGCI_VERSION))" @@ -244,7 +244,13 @@ lint: @golangci-lint run @echo "✅ Linting and static analysis passed" -# 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" +# Local CI parity for the gating jobs (build-and-test.yml + bindings-check.yml). +# Mirrors build-and-test's lint/test/build/--help so a broken main/CLI is caught +# locally, not in CI. The one CI step NOT folded in is the dry-run smoke: it's a +# backgrounded run killed after 5s and never asserts an exit code, so it's a weak, +# non-deterministic signal that's not worth a 5s+ wall-time tax on every verify. +# That step stays CI-only; see README "Before you push". +verify: lint test build check-bindings + @echo "🔍 Smoke-testing CLI entrypoint (--help)..." + @$(BUILD_DIR)/$(BINARY_NAME) --help > /dev/null + @echo "✅ verify passed (lint + test + build + --help + check-bindings)" diff --git a/README.md b/README.md index 9ef5b7b..c445480 100644 --- a/README.md +++ b/README.md @@ -175,11 +175,15 @@ blocks height=5191 time(p50=2s p99=5s max=8s) gas(p50=21000 p99=50000 max=100000 Run the full local CI gate in one command: ```bash -make verify # lint + test + check-bindings (exactly what CI gates on) +make verify # lint + test + build + CLI --help + check-bindings ``` -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: +`make verify` mirrors the gating CI jobs (`build-and-test`, `bindings-check`): +lint, test, compile the CLI, and a `--help` smoke. The only gating step it does +*not* run is CI's dry-run smoke (a backgrounded `seiload --dry-run` killed after +5s) — that asserts no exit code and stays CI-only, so a green `verify` is a strong +signal but not a literal guarantee of that one step. Install the pinned toolchain +once first so your local results match CI: ```bash make install-tools # full toolchain: Node (via nvm), solc, abigen, golangci-lint (pinned to v2.12.2) From 53dc995681e322cd17bd2877c176788446a4cd7c Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 15 Jun 2026 10:19:07 -0700 Subject: [PATCH 6/8] fix(make): normalize lint version v-prefix; serialize verify under -j Two Bugbot findings from the verify-build change: - lint guard: golangci-lint `version --short` reports `v2.12.2` while the pin is `2.12.2`, so a correctly-pinned dev got a false "version mismatch" warning. Strip a leading `v` before the compare; genuine mismatches still warn. - verify: as parallel prerequisites, `build` and `check-bindings` both write build/ and raced under `make -j`. Invoke the four gates as ordered sub-makes so verify is sequential regardless of -j, without a global .NOTPARALLEL. Co-Authored-By: Claude Opus 4.8 --- Makefile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e302e67..ef07622 100644 --- a/Makefile +++ b/Makefile @@ -238,6 +238,7 @@ test: 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); \ + have=$${have#v}; \ if [ -n "$$have" ] && [ "$$have" != "$(GOLANGCI_VERSION)" ]; then \ echo "⚠️ golangci-lint $$have on PATH != pinned $(GOLANGCI_VERSION). Run 'make install-lint' for CI parity."; \ fi @@ -250,7 +251,17 @@ lint: # backgrounded run killed after 5s and never asserts an exit code, so it's a weak, # non-deterministic signal that's not worth a 5s+ wall-time tax on every verify. # That step stays CI-only; see README "Before you push". -verify: lint test build check-bindings +# +# The gates are invoked as ordered sub-makes (not prerequisites) so `verify` +# runs them sequentially regardless of `-j`. As parallel prerequisites under +# `make -j`, `build` and `check-bindings` would run concurrently and both write +# the shared $(BUILD_DIR) tree (CLI binary vs. contract .abi/.bin from check- +# bindings' `-B generate-bindings`), racing each other. Sub-makes scope the +# serialization to `verify` alone, leaving the rest of the Makefile parallel- +# safe (unlike a global `.NOTPARALLEL`). `&&` short-circuits on first failure +# so a broken gate stops the chain with that gate's non-zero exit. +verify: + @$(MAKE) lint && $(MAKE) test && $(MAKE) build && $(MAKE) check-bindings @echo "🔍 Smoke-testing CLI entrypoint (--help)..." @$(BUILD_DIR)/$(BINARY_NAME) --help > /dev/null @echo "✅ verify passed (lint + test + build + --help + check-bindings)" From ed8348dc2190a0143dd093470d86241b4ede1b81 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 15 Jun 2026 13:54:20 -0700 Subject: [PATCH 7/8] ci: add check-lint-pin to gate golangci-lint version sync (#49) The pinned golangci-lint version lived in three files kept in sync by convention: Makefile (GOLANGCI_VERSION, source of truth), the workflow's golangci-lint-action `version:`, and .golangci.yml's header comment. A bump missing one file silently split local from CI. check-lint-pin derives the expected `v$(GOLANGCI_VERSION)` and asserts both other files match, naming the diverging file on mismatch. Wired as a build-and-test CI step (before the action that holds the divergent `version:`) and folded into `make verify` for local parity. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-and-test.yml | 10 +++++-- Makefile | 42 +++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3f2689b..f8167a2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -39,13 +39,17 @@ jobs: - name: Verify dependencies run: go mod verify + - name: Check golangci-lint version pin is in sync + run: make check-lint-pin + - name: Install golangci-lint uses: golangci/golangci-lint-action@v7 with: # 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. + # the Makefile and the version note in .golangci.yml's header comment. + # `latest` drifts between releases and shifts the default linter set, + # which is a "passes locally, fails CI" trap. Bump all three together; + # `make check-lint-pin` (run as a CI step above) enforces the sync. version: v2.12.2 - name: Run linting diff --git a/Makefile b/Makefile index ef07622..c355837 100644 --- a/Makefile +++ b/Makefile @@ -57,12 +57,12 @@ 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 install-lint clean help build-cli install setup-node build test lint verify +.PHONY: generate generate-bindings check-bindings check-lint-pin install-abigen install-lint clean help build-cli install setup-node build test lint verify # Default target help: @echo "Available targets:" - @echo " verify - Run the gating CI checks: lint + test + build + CLI --help + check-bindings" + @echo " verify - Run the gating CI checks: check-lint-pin + lint + test + build + CLI --help + check-bindings" @echo " build - Build the seiload CLI (alias for build-cli)" @echo " test - Run tests with coverage (race detector enabled)" @echo " lint - Run linting and static analysis (golangci-lint $(GOLANGCI_VERSION))" @@ -70,6 +70,7 @@ help: @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 " check-lint-pin - Fail if the golangci-lint version pin diverges across files" @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)" @@ -198,6 +199,39 @@ check-bindings: fi @echo "✅ Bindings are in sync with contracts" +# Drift check: GOLANGCI_VERSION (this Makefile) is the source of truth for the +# pinned golangci-lint. The same version is duplicated, by convention, in the +# workflow's golangci-lint-action `version:` and in .golangci.yml's header +# comment. A bump that misses a file silently splits local from CI ("passes +# locally, fails CI"). This target fails loudly, naming the diverging file. +# Mind the `v` prefix: Makefile holds 2.12.2; the others hold v2.12.2. +check-lint-pin: + @echo "🔍 Checking golangci-lint version pin is in sync..." + @expected="v$(GOLANGCI_VERSION)"; \ + rc=0; \ + wf=".github/workflows/build-and-test.yml"; \ + gc=".golangci.yml"; \ + wf_ver=$$(grep -E '^\s*version:\s*v[0-9]+\.[0-9]+\.[0-9]+' $$wf | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ -z "$$wf_ver" ]; then \ + echo "❌ $$wf: could not find a golangci-lint-action 'version: vX.Y.Z' line"; rc=1; \ + elif [ "$$wf_ver" != "$$expected" ]; then \ + echo "❌ $$wf: golangci-lint-action version $$wf_ver != Makefile $$expected"; rc=1; \ + fi; \ + gc_ver=$$(grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' $$gc | head -1); \ + if [ -z "$$gc_ver" ]; then \ + echo "❌ $$gc: could not find a 'vX.Y.Z' version reference in the header comment"; rc=1; \ + elif [ "$$gc_ver" != "$$expected" ]; then \ + echo "❌ $$gc: referenced version $$gc_ver != Makefile $$expected"; rc=1; \ + fi; \ + if [ $$rc -ne 0 ]; then \ + echo ""; \ + echo " golangci-lint pin diverged. Source of truth is GOLANGCI_VERSION"; \ + echo " ($(GOLANGCI_VERSION)) in the Makefile. Bump all three together:"; \ + echo " Makefile, $$wf, $$gc."; \ + exit 1; \ + fi; \ + echo "✅ golangci-lint pin in sync at $$expected (Makefile, $$wf, $$gc)" + # Install golangci-lint pinned to GOLANGCI_VERSION for CI parity (CI pins the # same version via golangci-lint-action). install-lint: @@ -261,7 +295,7 @@ lint: # safe (unlike a global `.NOTPARALLEL`). `&&` short-circuits on first failure # so a broken gate stops the chain with that gate's non-zero exit. verify: - @$(MAKE) lint && $(MAKE) test && $(MAKE) build && $(MAKE) check-bindings + @$(MAKE) check-lint-pin && $(MAKE) lint && $(MAKE) test && $(MAKE) build && $(MAKE) check-bindings @echo "🔍 Smoke-testing CLI entrypoint (--help)..." @$(BUILD_DIR)/$(BINARY_NAME) --help > /dev/null - @echo "✅ verify passed (lint + test + build + --help + check-bindings)" + @echo "✅ verify passed (check-lint-pin + lint + test + build + --help + check-bindings)" From 5bdf8fa3aa7a1548ef26777d919a6443e6cff7ef Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 15 Jun 2026 13:59:09 -0700 Subject: [PATCH 8/8] ci: harden check-lint-pin per cross-review (anchor grep, POSIX class, README parity) - Anchor the .golangci.yml version grep to the 'golangci-lint vX.Y.Z' header marker (was first-semver-anywhere; security+idiom LOW/NIT) so a future stray version token can't mislead it. - Use POSIX [[:space:]] instead of the GNU-ism \s, matching the repo's other version greps. - README: list check-lint-pin in the verify description for parity with make help. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index c355837..5fbf5a8 100644 --- a/Makefile +++ b/Makefile @@ -211,13 +211,13 @@ check-lint-pin: rc=0; \ wf=".github/workflows/build-and-test.yml"; \ gc=".golangci.yml"; \ - wf_ver=$$(grep -E '^\s*version:\s*v[0-9]+\.[0-9]+\.[0-9]+' $$wf | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + wf_ver=$$(grep -E '^[[:space:]]*version:[[:space:]]*v[0-9]+\.[0-9]+\.[0-9]+' $$wf | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ if [ -z "$$wf_ver" ]; then \ echo "❌ $$wf: could not find a golangci-lint-action 'version: vX.Y.Z' line"; rc=1; \ elif [ "$$wf_ver" != "$$expected" ]; then \ echo "❌ $$wf: golangci-lint-action version $$wf_ver != Makefile $$expected"; rc=1; \ fi; \ - gc_ver=$$(grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' $$gc | head -1); \ + gc_ver=$$(grep -oE 'golangci-lint v[0-9]+\.[0-9]+\.[0-9]+' $$gc | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ if [ -z "$$gc_ver" ]; then \ echo "❌ $$gc: could not find a 'vX.Y.Z' version reference in the header comment"; rc=1; \ elif [ "$$gc_ver" != "$$expected" ]; then \ diff --git a/README.md b/README.md index c445480..c89cf2c 100644 --- a/README.md +++ b/README.md @@ -175,11 +175,11 @@ blocks height=5191 time(p50=2s p99=5s max=8s) gas(p50=21000 p99=50000 max=100000 Run the full local CI gate in one command: ```bash -make verify # lint + test + build + CLI --help + check-bindings +make verify # check-lint-pin + lint + test + build + CLI --help + check-bindings ``` `make verify` mirrors the gating CI jobs (`build-and-test`, `bindings-check`): -lint, test, compile the CLI, and a `--help` smoke. The only gating step it does +the golangci-lint version-pin check, lint, test, compile the CLI, and a `--help` smoke. The only gating step it does *not* run is CI's dry-run smoke (a backgrounded `seiload --dry-run` killed after 5s) — that asserts no exit code and stays CI-only, so a green `verify` is a strong signal but not a literal guarantee of that one step. Install the pinned toolchain