From 11d7bcf5acdb7a1d5ec94a731409764d3ebfa794 Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Tue, 28 Apr 2026 13:37:27 +0100 Subject: [PATCH 1/3] chore: add golangci-lint with new-from-merge-base baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure golangci-lint v2 with the standard linter set (errcheck, govet, ineffassign, unused) plus forbidigo, which enforces the Ginkgo/Gomega-only test convention from .agents/coding-style.md by rejecting stdlib testing calls (t.Errorf, t.Fatalf, t.Run, ...). staticcheck is disabled — the codebase has many pre-existing QF-style suggestions not worth gating on. issues.new-from-merge-base = master makes the lint job a gate for new issues only; the ~1300 pre-existing baseline stays visible via 'make lint-all' for incremental cleanup. CI runs 'make lint'. Backends needing C/C++ headers we don't install in the lint runner are excluded via a deny list in the Makefile (backend/go/{piper,silero-vad, llm}, cmd/launcher). Discovery still flows through 'go list ./...', so new packages are scanned automatically. To make backend/go/{sam3-cpp,stablediffusion-ggml,whisper} typecheckable, move their .cpp/.h sources into cpp/ subdirs (matching qwen3-tts-cpp / acestep-cpp). Without this 'go list' rejects the package because Go does not allow .cpp alongside .go without cgo. Fix two real bugs found by lint in tests/integration/ (run only via 'make test-stores', not default CI): a stale zerolog reference left over from the slog migration (c37785b7) and an unused 'os' import. Assisted-by: Claude Code:Opus 4.7 (1M) [Bash] [Read] [Edit] [Write] Signed-off-by: Richard Palethorpe --- .agents/coding-style.md | 2 + .github/workflows/lint.yml | 33 ++++++++++++ .golangci.yml | 51 +++++++++++++++++++ Makefile | 34 ++++++++++++- backend/go/sam3-cpp/CMakeLists.txt | 2 +- backend/go/sam3-cpp/Makefile | 2 +- backend/go/sam3-cpp/{ => cpp}/gosam3.cpp | 0 backend/go/sam3-cpp/{ => cpp}/gosam3.h | 0 .../go/stablediffusion-ggml/CMakeLists.txt | 2 +- backend/go/stablediffusion-ggml/Makefile | 2 +- .../stablediffusion-ggml/{ => cpp}/gosd.cpp | 0 .../go/stablediffusion-ggml/{ => cpp}/gosd.h | 0 backend/go/whisper/CMakeLists.txt | 2 +- backend/go/whisper/Makefile | 2 +- backend/go/whisper/{ => cpp}/gowhisper.cpp | 0 backend/go/whisper/{ => cpp}/gowhisper.h | 0 tests/integration/integration_suite_test.go | 1 - tests/integration/stores_test.go | 2 - 18 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .golangci.yml rename backend/go/sam3-cpp/{ => cpp}/gosam3.cpp (100%) rename backend/go/sam3-cpp/{ => cpp}/gosam3.h (100%) rename backend/go/stablediffusion-ggml/{ => cpp}/gosd.cpp (100%) rename backend/go/stablediffusion-ggml/{ => cpp}/gosd.h (100%) rename backend/go/whisper/{ => cpp}/gowhisper.cpp (100%) rename backend/go/whisper/{ => cpp}/gowhisper.h (100%) diff --git a/.agents/coding-style.md b/.agents/coding-style.md index 6ead2ef98780..70cd25eee63c 100644 --- a/.agents/coding-style.md +++ b/.agents/coding-style.md @@ -48,6 +48,8 @@ All Go tests — including backend tests — must use [Ginkgo](https://onsi.gith Do not mix styles within a package. If you are extending tests in a package that already uses Ginkgo, keep using Ginkgo. If you find stdlib-style Go tests in the tree, treat them as tech debt to be migrated rather than as a pattern to follow. +This is enforced by `golangci-lint` via the `forbidigo` linter (see `.golangci.yml`); calls like `t.Errorf` / `t.Fatalf` / `t.Run` / `t.Skip` / `t.Logf` are flagged. Run `make lint` locally before submitting; the same check runs in CI (`.github/workflows/lint.yml`). + ## Documentation The project documentation is located in `docs/content`. When adding new features or changing existing functionality, it is crucial to update the documentation to reflect these changes. This helps users understand how to use the new capabilities and ensures the documentation stays relevant. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000000..98fc02ccaea5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,33 @@ +--- +name: 'lint' + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'examples/**' + - 'README.md' + - '**/*.md' + push: + branches: + - master + +concurrency: + group: ci-lint-${{ github.head_ref || github.ref }}-${{ github.repository }} + cancel-in-progress: true + +jobs: + golangci-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.x' + cache: false + - name: install golangci-lint + run: | + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ + | sh -s -- -b "$(go env GOPATH)/bin" v2.11.4 + - name: lint + run: make lint diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000000..3e157eaf1d7a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,51 @@ +version: "2" + +# Only issues introduced relative to master are reported. Pre-existing issues +# in the codebase do not fail the lint job; they're treated as a baseline that +# can be cleaned up incrementally. New code (added lines on a branch) is held +# to the full linter set. Locally, `make lint-all` overrides this and reports +# every issue. +issues: + new-from-merge-base: master + +linters: + default: standard + # staticcheck is noisy on this codebase (mostly QF style suggestions like + # "could use tagged switch" or "unnecessary fmt.Sprintf"). Re-enable + # selectively if a high-signal subset is identified. + disable: + - staticcheck + enable: + - forbidigo + settings: + forbidigo: + forbid: + - pattern: '^t\.Errorf$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Expect(...).To(...) instead of t.Errorf. See .agents/coding-style.md.' + - pattern: '^t\.Error$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Expect(...).To(...) instead of t.Error. See .agents/coding-style.md.' + - pattern: '^t\.Fatalf$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Expect(...).To(Succeed()) / Fail(...) instead of t.Fatalf. See .agents/coding-style.md.' + - pattern: '^t\.Fatal$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Expect(...).To(Succeed()) / Fail(...) instead of t.Fatal. See .agents/coding-style.md.' + - pattern: '^t\.Run$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Describe/Context/It instead of t.Run. See .agents/coding-style.md.' + - pattern: '^t\.Skip$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Skip(...) instead of t.Skip. See .agents/coding-style.md.' + - pattern: '^t\.Skipf$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Skip(...) instead of t.Skipf. See .agents/coding-style.md.' + - pattern: '^t\.SkipNow$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Skip(...) instead of t.SkipNow. See .agents/coding-style.md.' + - pattern: '^t\.Logf$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use GinkgoWriter / fmt.Fprintf(GinkgoWriter, ...) instead of t.Logf. See .agents/coding-style.md.' + - pattern: '^t\.Log$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use GinkgoWriter / fmt.Fprintln(GinkgoWriter, ...) instead of t.Log. See .agents/coding-style.md.' + - pattern: '^t\.Fail$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Fail(...) instead of t.Fail. See .agents/coding-style.md.' + - pattern: '^t\.FailNow$' + msg: 'LocalAI tests must use Ginkgo/Gomega; use Fail(...) instead of t.FailNow. See .agents/coding-style.md.' + exclusions: + paths: + # Upstream whisper.cpp source tree fetched by the whisper backend Makefile. + - 'backend/go/whisper/sources' + - 'docs/' diff --git a/Makefile b/Makefile index a76f7975ac38..f9c2d87ef497 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ endif TEST_PATHS?=./api/... ./pkg/... ./core/... -.PHONY: all test build vendor +.PHONY: all test build vendor lint lint-all all: help @@ -163,6 +163,38 @@ test: prepare-test OPUS_SHIM_LIBRARY=$(abspath ./pkg/opus/shim/libopusshim.so) \ $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --flake-attempts $(TEST_FLAKES) --fail-fast -v -r $(TEST_PATHS) +######################################################## +## Lint +######################################################## +## Runs golangci-lint with config from .golangci.yml. Includes the standard +## linter set plus forbidigo, which enforces the Ginkgo/Gomega-only test +## convention documented in .agents/coding-style.md. +## +## LINT_EXCLUDE_DIRS_RE matches directories whose Go packages can't typecheck +## without C/C++ headers we don't install in the lint runner (cgo wrappers +## around llama.cpp, piper/spdlog, silero-vad/onnxruntime, and Fyne/OpenGL for +## the launcher). Their compile-time correctness is enforced by their own +## build pipelines. Keep this as a deny list — `go list ./...` discovers +## everything else automatically, so new packages are scanned by default. +LINT_EXCLUDE_DIRS_RE=/(backend/go/(piper|silero-vad|llm)|cmd/launcher)(/|$$) + +lint: + @command -v golangci-lint >/dev/null 2>&1 || { \ + echo 'golangci-lint not installed. Install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest'; \ + exit 1; \ + } + golangci-lint run $$(go list -e -f '{{.Dir}}' ./... | grep -vE '$(LINT_EXCLUDE_DIRS_RE)') + +## Like `lint` but reports every issue, including the pre-existing baseline +## that `lint` ignores via .golangci.yml's new-from-merge-base. Use this to +## see what's available to clean up. +lint-all: + @command -v golangci-lint >/dev/null 2>&1 || { \ + echo 'golangci-lint not installed. Install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest'; \ + exit 1; \ + } + golangci-lint run --new=false --new-from-merge-base= --new-from-rev= $$(go list -e -f '{{.Dir}}' ./... | grep -vE '$(LINT_EXCLUDE_DIRS_RE)') + ######################################################## ## E2E AIO tests (uses standard image with pre-configured models) ######################################################## diff --git a/backend/go/sam3-cpp/CMakeLists.txt b/backend/go/sam3-cpp/CMakeLists.txt index c43569d5014f..73c8e2d01e35 100644 --- a/backend/go/sam3-cpp/CMakeLists.txt +++ b/backend/go/sam3-cpp/CMakeLists.txt @@ -10,7 +10,7 @@ set(SAM3_BUILD_TESTS OFF CACHE BOOL "Disable sam3.cpp tests" FORCE) add_subdirectory(./sources/sam3.cpp) -add_library(gosam3 MODULE gosam3.cpp) +add_library(gosam3 MODULE cpp/gosam3.cpp) target_link_libraries(gosam3 PRIVATE sam3 ggml) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) diff --git a/backend/go/sam3-cpp/Makefile b/backend/go/sam3-cpp/Makefile index ed0aa3c6e19c..53b0dfb5e6ab 100644 --- a/backend/go/sam3-cpp/Makefile +++ b/backend/go/sam3-cpp/Makefile @@ -111,7 +111,7 @@ libgosam3-fallback.so: sources/sam3.cpp SO_TARGET=libgosam3-fallback.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgosam3-custom rm -rfv build* -libgosam3-custom: CMakeLists.txt gosam3.cpp gosam3.h +libgosam3-custom: CMakeLists.txt cpp/gosam3.cpp cpp/gosam3.h mkdir -p build-$(SO_TARGET) && \ cd build-$(SO_TARGET) && \ cmake .. $(CMAKE_ARGS) && \ diff --git a/backend/go/sam3-cpp/gosam3.cpp b/backend/go/sam3-cpp/cpp/gosam3.cpp similarity index 100% rename from backend/go/sam3-cpp/gosam3.cpp rename to backend/go/sam3-cpp/cpp/gosam3.cpp diff --git a/backend/go/sam3-cpp/gosam3.h b/backend/go/sam3-cpp/cpp/gosam3.h similarity index 100% rename from backend/go/sam3-cpp/gosam3.h rename to backend/go/sam3-cpp/cpp/gosam3.h diff --git a/backend/go/stablediffusion-ggml/CMakeLists.txt b/backend/go/stablediffusion-ggml/CMakeLists.txt index 41b52c18d2e2..35a852c3101b 100644 --- a/backend/go/stablediffusion-ggml/CMakeLists.txt +++ b/backend/go/stablediffusion-ggml/CMakeLists.txt @@ -4,7 +4,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(./sources/stablediffusion-ggml.cpp) -add_library(gosd MODULE gosd.cpp) +add_library(gosd MODULE cpp/gosd.cpp) target_link_libraries(gosd PRIVATE stable-diffusion ggml) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) diff --git a/backend/go/stablediffusion-ggml/Makefile b/backend/go/stablediffusion-ggml/Makefile index c553c07587b9..aef05d4e65e0 100644 --- a/backend/go/stablediffusion-ggml/Makefile +++ b/backend/go/stablediffusion-ggml/Makefile @@ -119,7 +119,7 @@ libgosd-fallback.so: sources/stablediffusion-ggml.cpp SO_TARGET=libgosd-fallback.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgosd-custom rm -rfv build* -libgosd-custom: CMakeLists.txt gosd.cpp gosd.h +libgosd-custom: CMakeLists.txt cpp/gosd.cpp cpp/gosd.h mkdir -p build-$(SO_TARGET) && \ cd build-$(SO_TARGET) && \ cmake .. $(CMAKE_ARGS) && \ diff --git a/backend/go/stablediffusion-ggml/gosd.cpp b/backend/go/stablediffusion-ggml/cpp/gosd.cpp similarity index 100% rename from backend/go/stablediffusion-ggml/gosd.cpp rename to backend/go/stablediffusion-ggml/cpp/gosd.cpp diff --git a/backend/go/stablediffusion-ggml/gosd.h b/backend/go/stablediffusion-ggml/cpp/gosd.h similarity index 100% rename from backend/go/stablediffusion-ggml/gosd.h rename to backend/go/stablediffusion-ggml/cpp/gosd.h diff --git a/backend/go/whisper/CMakeLists.txt b/backend/go/whisper/CMakeLists.txt index 60cc178f2b23..36c529006b2d 100644 --- a/backend/go/whisper/CMakeLists.txt +++ b/backend/go/whisper/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_subdirectory(./sources/whisper.cpp) -add_library(gowhisper MODULE gowhisper.cpp) +add_library(gowhisper MODULE cpp/gowhisper.cpp) target_link_libraries(gowhisper PRIVATE whisper ggml) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) diff --git a/backend/go/whisper/Makefile b/backend/go/whisper/Makefile index c737f3b0e17a..e57189a81cd1 100644 --- a/backend/go/whisper/Makefile +++ b/backend/go/whisper/Makefile @@ -111,7 +111,7 @@ libgowhisper-fallback.so: sources/whisper.cpp SO_TARGET=libgowhisper-fallback.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgowhisper-custom rm -rfv build* -libgowhisper-custom: CMakeLists.txt gowhisper.cpp gowhisper.h +libgowhisper-custom: CMakeLists.txt cpp/gowhisper.cpp cpp/gowhisper.h mkdir -p build-$(SO_TARGET) && \ cd build-$(SO_TARGET) && \ cmake .. $(CMAKE_ARGS) && \ diff --git a/backend/go/whisper/gowhisper.cpp b/backend/go/whisper/cpp/gowhisper.cpp similarity index 100% rename from backend/go/whisper/gowhisper.cpp rename to backend/go/whisper/cpp/gowhisper.cpp diff --git a/backend/go/whisper/gowhisper.h b/backend/go/whisper/cpp/gowhisper.h similarity index 100% rename from backend/go/whisper/gowhisper.h rename to backend/go/whisper/cpp/gowhisper.h diff --git a/tests/integration/integration_suite_test.go b/tests/integration/integration_suite_test.go index 0e5ab3a2c55f..6ed3622924df 100644 --- a/tests/integration/integration_suite_test.go +++ b/tests/integration/integration_suite_test.go @@ -1,7 +1,6 @@ package integration_test import ( - "os" "testing" "github.com/mudler/xlog" diff --git a/tests/integration/stores_test.go b/tests/integration/stores_test.go index 283a3876b31f..9b977d4c2381 100644 --- a/tests/integration/stores_test.go +++ b/tests/integration/stores_test.go @@ -39,8 +39,6 @@ var _ = Describe("Integration tests for the stores backend(s) and internal APIs" BeforeEach(func() { var err error - zerolog.SetGlobalLevel(zerolog.DebugLevel) - tmpdir, err = os.MkdirTemp("", "") Expect(err).ToNot(HaveOccurred()) From 30e779bea781d288be81b42208a2fc656e40d6a9 Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Tue, 28 Apr 2026 15:41:04 +0100 Subject: [PATCH 2/3] ci(lint): generate proto sources and fetch full history The lint job was failing for two reasons: - pkg/grpc/proto/*.go is generated, not checked in. Several packages import it, so without 'make protogen-go' typecheck fails project-wide with "no required module provides package github.com/mudler/LocalAI/ pkg/grpc/proto". - golangci-lint's new-from-merge-base needs to git-merge-base the PR against master, but actions/checkout's default shallow clone doesn't fetch master. fetch-depth: 0 brings full history; the config now references origin/master (the remote-tracking branch that survives the shallow checkout) instead of bare master (which doesn't exist locally after checkout). Assisted-by: Claude Code:Opus 4.7 (1M) [Bash] [Read] [Edit] [Write] Signed-off-by: Richard Palethorpe --- .github/workflows/lint.yml | 8 ++++++++ .golangci.yml | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 98fc02ccaea5..9642d5aca066 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,6 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + # Full history so golangci-lint's new-from-merge-base can reach + # origin/master and compute the diff against it. + fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: '1.26.x' @@ -29,5 +33,9 @@ jobs: run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ | sh -s -- -b "$(go env GOPATH)/bin" v2.11.4 + - name: generate grpc proto sources + # pkg/grpc/proto/*.go is generated, not checked in. Several packages + # import it, so without this step typecheck fails project-wide. + run: make protogen-go - name: lint run: make lint diff --git a/.golangci.yml b/.golangci.yml index 3e157eaf1d7a..aa82a810bc64 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,7 +6,9 @@ version: "2" # to the full linter set. Locally, `make lint-all` overrides this and reports # every issue. issues: - new-from-merge-base: master + # origin/master because in shallow CI checkouts only the remote-tracking + # branch exists; a bare 'master' ref isn't reachable locally. + new-from-merge-base: origin/master linters: default: standard From c7972d1d3baf5e0840b63686288a9f9468f86e8b Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Tue, 28 Apr 2026 16:08:37 +0100 Subject: [PATCH 3/3] ci(lint): stub react-ui/dist for go:embed glob core/http/app.go has //go:embed react-ui/dist/*. The glob must match at least one non-hidden entry or typecheck fails the whole core/http package. We don't need the real React bundle to lint Go code, so just touch an empty index.html to satisfy the embed. Assisted-by: Claude Code:Opus 4.7 (1M) [Bash] [Read] [Edit] [Write] Signed-off-by: Richard Palethorpe --- .github/workflows/lint.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9642d5aca066..fb7797b4d15a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,5 +37,12 @@ jobs: # pkg/grpc/proto/*.go is generated, not checked in. Several packages # import it, so without this step typecheck fails project-wide. run: make protogen-go + - name: stub react-ui dist for go:embed + # core/http/app.go has //go:embed react-ui/dist/*; the glob needs at + # least one non-hidden entry to satisfy typecheck. We don't run + # `make react-ui` here because lint doesn't need the real bundle. + run: | + mkdir -p core/http/react-ui/dist + touch core/http/react-ui/dist/index.html - name: lint run: make lint