From 05ffd66d46e0d1b1fd8b128a9abf09ddaeacd09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mom=C4=8Dilo=20Miladinovi=C4=87?= Date: Wed, 19 Aug 2026 10:58:50 +0200 Subject: [PATCH 1/6] chore: add versioned pre-push hook running the misspell linter --- Makefile | 5 ++++ scripts/git-hooks/pre-push | 56 ++++++++++++++++++++++++++++++++++++++ tool.mod | 1 + 3 files changed, 62 insertions(+) create mode 100755 scripts/git-hooks/pre-push diff --git a/Makefile b/Makefile index bdc99977a6..1551f57a88 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,11 @@ ssvsigner-golangci-lint: deadcode-lint: ./scripts/deadcode.sh +.PHONY: install-hooks +install-hooks: + git config core.hooksPath scripts/git-hooks + @echo "git hooks installed (core.hooksPath -> scripts/git-hooks)" + .PHONY: ssvsigner-boundary-lint ssvsigner-boundary-lint: ./scripts/ssvsigner_boundary.sh diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100755 index 0000000000..dc692bdd4e --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,56 @@ +#!/bin/sh +# Pre-push hook: run the misspell linter (locale US, same as .golangci.yaml's +# misspell config) on the .go/.md files this push introduces or modifies, so +# spelling failures surface locally instead of on the PR's lint check. +# +# Install with: make install-hooks +# Bypass for a one-off push: git push --no-verify + +root=$(git rev-parse --show-toplevel) || exit 0 + +# go tool compiles misspell from tool.mod on first use and caches it; go may +# not be on PATH in GUI-spawned shells, so fall back to the default install. +if command -v go >/dev/null 2>&1; then + GO=go +elif [ -x /usr/local/go/bin/go ]; then + GO=/usr/local/go/bin/go +else + exit 0 +fi + +ZERO=0000000000000000000000000000000000000000 +status=0 + +while read -r local_ref local_sha remote_ref remote_sha; do + [ "$local_sha" = "$ZERO" ] && continue # deleting a remote ref + + if [ "$remote_sha" = "$ZERO" ]; then + # New remote branch: diff against the merge-base with stage (or main). + base=$(git merge-base "$local_sha" origin/stage 2>/dev/null) || + base=$(git merge-base "$local_sha" origin/main 2>/dev/null) || continue + else + base=$remote_sha + fi + + files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- '*.go' '*.md') + [ -z "$files" ] && continue + + # Check the blobs at local_sha, not the working tree, so uncommitted edits + # don't mask (or fake) findings in what's actually being pushed. + for f in $files; do + out=$(git show "$local_sha:$f" 2>/dev/null | + GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error 2>&1) + if [ -n "$out" ]; then + echo "pre-push: misspell found issues in $f:" >&2 + echo "$out" | sed "s|^stdin| $f|" >&2 + status=1 + fi + done +done + +if [ "$status" -ne 0 ]; then + echo "" >&2 + echo "pre-push: fix the spellings above and amend/commit — the CI lint check (golangci-lint misspell) fails on them." >&2 + echo "pre-push: bypass once with: git push --no-verify" >&2 +fi +exit $status diff --git a/tool.mod b/tool.mod index 08abb9b3ec..fcddbaf74d 100644 --- a/tool.mod +++ b/tool.mod @@ -7,6 +7,7 @@ tool ( github.com/ethereum/go-ethereum/cmd/abigen github.com/ferranbt/fastssz/sszgen github.com/golangci/golangci-lint/v2/cmd/golangci-lint + github.com/golangci/misspell/cmd/misspell github.com/swaggo/swag/cmd/swag go.uber.org/mock/mockgen golang.org/x/tools/cmd/deadcode From 6026fa65f0c311fd09272d7fa034ad8b975de969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mom=C4=8Dilo=20Miladinovi=C4=87?= Date: Wed, 19 Aug 2026 14:13:50 +0200 Subject: [PATCH 2/6] chore: extend the pre-push hook with golangci-lint on changed packages --- scripts/git-hooks/pre-push | 53 +++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index dc692bdd4e..b993561f35 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -1,15 +1,21 @@ #!/bin/sh -# Pre-push hook: run the misspell linter (locale US, same as .golangci.yaml's -# misspell config) on the .go/.md files this push introduces or modifies, so -# spelling failures surface locally instead of on the PR's lint check. +# Pre-push hook: lint what this push introduces or modifies, so lint failures +# surface locally instead of on the PR's lint check. Two stages: +# +# 1. misspell (locale US, same as .golangci.yaml's misspell config) on the +# changed .go/.md blobs at the pushed commit. +# 2. golangci-lint (the same config CI's `make lint` uses) on the packages +# containing changed .go files. Unlike misspell it needs full packages on +# disk for type-checking, so it lints the checkout rather than the pushed +# blobs and only runs when pushing the checked-out HEAD. # # Install with: make install-hooks # Bypass for a one-off push: git push --no-verify root=$(git rev-parse --show-toplevel) || exit 0 -# go tool compiles misspell from tool.mod on first use and caches it; go may -# not be on PATH in GUI-spawned shells, so fall back to the default install. +# go tool compiles the linters from tool.mod on first use and caches them; go +# may not be on PATH in GUI-spawned shells, so fall back to the default install. if command -v go >/dev/null 2>&1; then GO=go elif [ -x /usr/local/go/bin/go ]; then @@ -20,6 +26,8 @@ fi ZERO=0000000000000000000000000000000000000000 status=0 +head_sha=$(git rev-parse HEAD) +lint_dirs="" while read -r local_ref local_sha remote_ref remote_sha; do [ "$local_sha" = "$ZERO" ] && continue # deleting a remote ref @@ -46,11 +54,44 @@ while read -r local_ref local_sha remote_ref remote_sha; do status=1 fi done + + # Collect the packages holding changed .go files for the golangci-lint + # stage. Only when this ref is the checked-out HEAD — the checkout is what + # gets linted, so for any other ref the results wouldn't describe the push. + [ "$local_sha" = "$head_sha" ] || continue + for f in $files; do + case $f in + *.go) ;; + *) continue ;; + esac + d=./$(dirname "$f") + case $d in + # Nested modules: CI's `make lint` golangci run covers the root module + # only, and golangci-lint can't load their packages from here anyway. + ./ssvsigner | ./ssvsigner/*) continue ;; + ./scripts/differ | ./scripts/differ/*) continue ;; + esac + [ -d "$root/${d#./}" ] || continue # dir may be gone (renames) + case " $lint_dirs " in + *" $d "*) ;; + *) lint_dirs="$lint_dirs $d" ;; + esac + done done +# golangci-lint stage: staticcheck & friends on the changed packages, scoped so +# a push costs seconds, not CI's full-repo minutes. It sees the working tree, +# so uncommitted edits in these packages show up here. +if [ -n "$lint_dirs" ]; then + # shellcheck disable=SC2086 # lint_dirs is word-split on purpose + if ! (cd "$root" && GOWORK=off "$GO" tool -modfile="$root/tool.mod" golangci-lint run $lint_dirs >&2); then + status=1 + fi +fi + if [ "$status" -ne 0 ]; then echo "" >&2 - echo "pre-push: fix the spellings above and amend/commit — the CI lint check (golangci-lint misspell) fails on them." >&2 + echo "pre-push: fix the issues above and amend/commit — the CI lint check (make lint) fails on them." >&2 echo "pre-push: bypass once with: git push --no-verify" >&2 fi exit $status From 231d6b758476b859b5b8729fac08478d3af06a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mom=C4=8Dilo=20Miladinovi=C4=87?= Date: Wed, 19 Aug 2026 15:47:45 +0200 Subject: [PATCH 3/6] chore: warn instead of silently skipping when no merge-base is found --- scripts/git-hooks/pre-push | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index b993561f35..9213cc8dfa 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -35,7 +35,10 @@ while read -r local_ref local_sha remote_ref remote_sha; do if [ "$remote_sha" = "$ZERO" ]; then # New remote branch: diff against the merge-base with stage (or main). base=$(git merge-base "$local_sha" origin/stage 2>/dev/null) || - base=$(git merge-base "$local_sha" origin/main 2>/dev/null) || continue + base=$(git merge-base "$local_sha" origin/main 2>/dev/null) || { + echo "pre-push: no merge-base with origin/stage or origin/main — skipping lint for $local_ref" >&2 + continue + } else base=$remote_sha fi From f43e9700692f0b62cfd2e9e20649ec99ac527405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mom=C4=8Dilo=20Miladinovi=C4=87?= Date: Wed, 19 Aug 2026 15:49:03 +0200 Subject: [PATCH 4/6] chore: probe misspell once so tool failures aren't reported as findings --- scripts/git-hooks/pre-push | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index 9213cc8dfa..71d8a80d9b 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -28,6 +28,7 @@ ZERO=0000000000000000000000000000000000000000 status=0 head_sha=$(git rev-parse HEAD) lint_dirs="" +misspell_ok="" # probed lazily on the first push with changed files while read -r local_ref local_sha remote_ref remote_sha; do [ "$local_sha" = "$ZERO" ] && continue # deleting a remote ref @@ -46,17 +47,31 @@ while read -r local_ref local_sha remote_ref remote_sha; do files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- '*.go' '*.md') [ -z "$files" ] && continue + # go tool failures (resolution, compile, module download) must not be + # mistaken for spelling findings, so probe misspell once up front; if it + # can't run, warn and skip this stage instead of blocking the push. + if [ -z "$misspell_ok" ]; then + if printf '' | GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error >/dev/null 2>&1; then + misspell_ok=y + else + misspell_ok=n + echo "pre-push: cannot run misspell via 'go tool' — skipping the spelling stage" >&2 + fi + fi + # Check the blobs at local_sha, not the working tree, so uncommitted edits # don't mask (or fake) findings in what's actually being pushed. - for f in $files; do - out=$(git show "$local_sha:$f" 2>/dev/null | - GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error 2>&1) - if [ -n "$out" ]; then - echo "pre-push: misspell found issues in $f:" >&2 - echo "$out" | sed "s|^stdin| $f|" >&2 - status=1 - fi - done + if [ "$misspell_ok" = y ]; then + for f in $files; do + out=$(git show "$local_sha:$f" 2>/dev/null | + GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error 2>&1) + if [ -n "$out" ]; then + echo "pre-push: misspell found issues in $f:" >&2 + echo "$out" | sed "s|^stdin| $f|" >&2 + status=1 + fi + done + fi # Collect the packages holding changed .go files for the golangci-lint # stage. Only when this ref is the checked-out HEAD — the checkout is what From 064d3f35ffaa84c8a42cbc733679de332b538955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mom=C4=8Dilo=20Miladinovi=C4=87?= Date: Wed, 19 Aug 2026 15:51:40 +0200 Subject: [PATCH 5/6] chore: split the changed-file list on newlines and disable globbing --- scripts/git-hooks/pre-push | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index 71d8a80d9b..dfd90c52ae 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -26,6 +26,14 @@ fi ZERO=0000000000000000000000000000000000000000 status=0 + +# Changed paths may contain spaces or glob characters: iterate $files with a +# newline-only IFS (restored to the default around the outer read, which needs +# it to split its fields) and keep pathname expansion off for the whole run. +set -f +nl=' +' +oIFS=$IFS head_sha=$(git rev-parse HEAD) lint_dirs="" misspell_ok="" # probed lazily on the first push with changed files @@ -62,6 +70,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do # Check the blobs at local_sha, not the working tree, so uncommitted edits # don't mask (or fake) findings in what's actually being pushed. if [ "$misspell_ok" = y ]; then + IFS=$nl for f in $files; do out=$(git show "$local_sha:$f" 2>/dev/null | GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error 2>&1) @@ -71,12 +80,14 @@ while read -r local_ref local_sha remote_ref remote_sha; do status=1 fi done + IFS=$oIFS fi # Collect the packages holding changed .go files for the golangci-lint # stage. Only when this ref is the checked-out HEAD — the checkout is what # gets linted, so for any other ref the results wouldn't describe the push. [ "$local_sha" = "$head_sha" ] || continue + IFS=$nl for f in $files; do case $f in *.go) ;; @@ -95,6 +106,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do *) lint_dirs="$lint_dirs $d" ;; esac done + IFS=$oIFS done # golangci-lint stage: staticcheck & friends on the changed packages, scoped so From c29d588122879a3561b27acaa008dd19b170a4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mom=C4=8Dilo=20Miladinovi=C4=87?= Date: Fri, 21 Aug 2026 10:45:36 +0200 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20lint=20changed=20ssvsigner=20packa?= =?UTF-8?q?ges=20too=20=E2=80=94=20CI's=20make=20lint=20covers=20that=20mo?= =?UTF-8?q?dule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/git-hooks/pre-push | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index dfd90c52ae..32b6081030 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -36,6 +36,7 @@ nl=' oIFS=$IFS head_sha=$(git rev-parse HEAD) lint_dirs="" +ssvsigner_lint_dirs="" misspell_ok="" # probed lazily on the first push with changed files while read -r local_ref local_sha remote_ref remote_sha; do @@ -94,13 +95,22 @@ while read -r local_ref local_sha remote_ref remote_sha; do *) continue ;; esac d=./$(dirname "$f") + [ -d "$root/${d#./}" ] || continue # dir may be gone (renames) case $d in - # Nested modules: CI's `make lint` golangci run covers the root module - # only, and golangci-lint can't load their packages from here anyway. - ./ssvsigner | ./ssvsigner/*) continue ;; + # ssvsigner is a nested module CI lints too (make lint runs + # ssvsigner-golangci-lint from ssvsigner/), and its packages can't be + # loaded from the root, so collect its dirs for a separate run. + ./ssvsigner | ./ssvsigner/*) + sd=.${d#./ssvsigner} + case " $ssvsigner_lint_dirs " in + *" $sd "*) ;; + *) ssvsigner_lint_dirs="$ssvsigner_lint_dirs $sd" ;; + esac + continue + ;; + # Nested module CI's `make lint` doesn't cover. ./scripts/differ | ./scripts/differ/*) continue ;; esac - [ -d "$root/${d#./}" ] || continue # dir may be gone (renames) case " $lint_dirs " in *" $d "*) ;; *) lint_dirs="$lint_dirs $d" ;; @@ -118,6 +128,14 @@ if [ -n "$lint_dirs" ]; then status=1 fi fi +if [ -n "$ssvsigner_lint_dirs" ]; then + # Mirrors make lint's ssvsigner-golangci-lint: the nested module is linted + # from its own directory, with the root's config and tool.mod. + # shellcheck disable=SC2086 # ssvsigner_lint_dirs is word-split on purpose + if ! (cd "$root/ssvsigner" && GOWORK=off "$GO" tool -modfile="$root/tool.mod" golangci-lint run -c "$root/.golangci.yaml" $ssvsigner_lint_dirs >&2); then + status=1 + fi +fi if [ "$status" -ne 0 ]; then echo "" >&2