From 9204d607bee18fd6b97aec5a6c185732e14833ee Mon Sep 17 00:00:00 2001 From: Abdel Fane <32418586+thebenignhacker@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:51:47 -0600 Subject: [PATCH] Run golangci-lint v1.64.8, the release the CI lint job installs, from make lint and the contributor docs, since golangci-lint 2.x does not load the v1-format .golangci.yml (#61) The Makefile lint target, CONTRIBUTING.md lines 46, 55 and 165 and docs/development.md line 26 ran the golangci-lint on PATH. .golangci.yml is in the v1 configuration format: golangci-lint 2.12.2 stops on it with "can't load config: unsupported version of the configuration" and exit code 3, and the run command of golangci-lint 2.0.0 refuses a file without version: "2". The CI lint job asks golangci-lint-action@v4 for "latest", and its log reads "Requested golangci-lint 'latest', using 'v1.64.8'". make lint now runs golangci-lint through go run at GOLANGCI_LINT_VERSION, set to v1.64.8 in the Makefile, and both pages tell contributors to run make lint. CONTRIBUTING.md also gives the go run command for the same release without make. With golangci-lint 2.12.2 on PATH, make lint exited 0; the same command with -v named .golangci.yml as its config, listed six active linters (errcheck, gosimple, govet, ineffassign, staticcheck, unused) and reported 0 issues after processing. The configuration stays in the v1 format: golangci-lint v1.64.8 refuses a file that carries a version key ("you are using a configuration file for golangci-lint v2 with golangci-lint v1"), so the CI lint job would fail until it installs a 2.x release, and 2.12.2 reports 8 staticcheck issues with the file golangci-lint migrate writes. TestDocsLintCommandRunsTheGolangciLintReleaseTheConfigIsWrittenFor fails when the Makefile pins no release, when the pinned major version differs from the format of .golangci.yml, or when the Makefile, README.md, CONTRIBUTING.md or a page under docs has a golangci-lint run command that does not name the pinned release. On the tree before this change it reports the missing pin, Makefile:38, CONTRIBUTING.md:46, 55 and 165 and docs/development.md:26, and it passes after. TestGolangciLintProblems checks those rules on sample configurations, Makefiles and docs. Refs #61 Remaining: pin the CI lint job's golangci-lint version in .github/workflows/ci.yml, or move .golangci.yml to the v2 format together with a golangci-lint-action release that installs 2.x. --- CHANGELOG.md | 10 ++ CONTRIBUTING.md | 12 +- Makefile | 6 +- cmd/cryptodeps/docs_lint_command_test.go | 193 +++++++++++++++++++++++ docs/development.md | 2 +- 5 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 cmd/cryptodeps/docs_lint_command_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 42416c3..3299ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -271,6 +271,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [license text](https://www.apache.org/licenses/LICENSE-2.0.txt) word for word. +- **`make lint` and the lint command in CONTRIBUTING.md and + docs/development.md ran the golangci-lint on PATH, and golangci-lint 2.x + does not load the committed `.golangci.yml`.** That file is in the v1 + configuration format: golangci-lint 2.12.2 stops on it with `can't load + config: unsupported version of the configuration` and exit code 3, while the + CI lint job installs v1.64.8. `make lint` now uses `go run` to build and run + v1.64.8, set as `GOLANGCI_LINT_VERSION` in the Makefile, and both pages tell + contributors to run `make lint`. CONTRIBUTING.md also gives the `go run` + command for the same release without make. + ## [1.3.1] - 2026-10-17 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d265605..e4d0c71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,13 @@ go fmt ./... gofumpt -w . # Run linter -golangci-lint run +make lint +``` + +`.golangci.yml` is in the golangci-lint v1 configuration format, which golangci-lint 2.x releases do not load: 2.12.2 stops with `can't load config: unsupported version of the configuration` and exits 3 without linting. `make lint` uses `go run` to build and run the golangci-lint release the CI lint job installs (`GOLANGCI_LINT_VERSION` in the Makefile), so the golangci-lint on your PATH is not used. Without make, run the same release: + +```bash +go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 run ./... ``` ## Contributing Code @@ -52,7 +58,7 @@ golangci-lint run 2. Create a feature branch: `git checkout -b feature/my-feature` 3. Make your changes 4. Run tests: `go test -race ./...` -5. Run linter: `golangci-lint run` +5. Run linter: `make lint` 6. Commit with a descriptive message 7. Push and open a Pull Request @@ -162,7 +168,7 @@ For security issues, please email security@csnp.org instead of opening a public ### Before Submitting - [ ] Tests pass: `go test -race ./...` -- [ ] Linter passes: `golangci-lint run` +- [ ] Linter passes: `make lint` - [ ] Code is formatted: `go fmt ./...` - [ ] Documentation updated if needed - [ ] Commit messages follow conventional commits diff --git a/Makefile b/Makefile index d8164fd..aeb42ba 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,10 @@ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none") DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" +# golangci-lint release that make lint builds and runs. It is the release the +# CI lint job installs, and .golangci.yml is in its v1 configuration format. +GOLANGCI_LINT_VERSION := v1.64.8 + # Default target all: build @@ -35,7 +39,7 @@ coverage: test ## lint: Run linter lint: - golangci-lint run ./... + go run github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) run ./... ## fmt: Format code fmt: diff --git a/cmd/cryptodeps/docs_lint_command_test.go b/cmd/cryptodeps/docs_lint_command_test.go new file mode 100644 index 0000000..d848e24 --- /dev/null +++ b/cmd/cryptodeps/docs_lint_command_test.go @@ -0,0 +1,193 @@ +// Copyright 2025-2026 CyberSecurity NonProfit (CSNP) +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "io/fs" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +// golangciLintPin matches the Makefile line that sets the golangci-lint release +// make lint runs. The second group is its major version. +var golangciLintPin = regexp.MustCompile(`(?m)^GOLANGCI_LINT_VERSION\s*[:?]?=\s*(v(\d+)\.\d+\.\d+)\s*$`) + +// golangciLintRun matches a command that runs golangci-lint. The group is the +// release after the @ that go run takes, when there is one. +var golangciLintRun = regexp.MustCompile(`golangci-lint(?:@(\S+))?\s+run\b`) + +// lintDocs are the places a contributor takes the lint command from. +var lintDocs = []string{"Makefile", "README.md", "CONTRIBUTING.md", "docs"} + +// golangciLintProblems returns what is wrong with the golangci-lint commands in +// files, which maps a repository path to its text and has to hold the Makefile, +// given the text of .golangci.yml. The Makefile has to pin one release as +// GOLANGCI_LINT_VERSION, with the major version of the configuration format, +// and each command golangciLintRun finds has to name that release. +func golangciLintProblems(config string, files map[string]string) ([]string, error) { + var cfg struct { + Version any `yaml:"version"` + } + if err := yaml.Unmarshal([]byte(config), &cfg); err != nil { + return nil, fmt.Errorf("parsing .golangci.yml: %w", err) + } + format := "1" + if cfg.Version != nil { + format = fmt.Sprint(cfg.Version) + } + + var problems []string + pin, pinMajor := "", "" + if m := golangciLintPin.FindStringSubmatch(files["Makefile"]); m != nil { + pin, pinMajor = m[1], m[2] + } + switch { + case pin == "": + problems = append(problems, "the Makefile sets GOLANGCI_LINT_VERSION to no release tag "+ + "(vMAJOR.MINOR.PATCH), so make lint pins no golangci-lint release") + case pinMajor != format: + problems = append(problems, fmt.Sprintf("the Makefile pins golangci-lint %s, but .golangci.yml "+ + "is in the v%s configuration format", pin, format)) + } + + paths := make([]string, 0, len(files)) + for path := range files { + paths = append(paths, path) + } + sort.Strings(paths) + inMakefile := 0 + for _, path := range paths { + for i, line := range strings.Split(files[path], "\n") { + for _, m := range golangciLintRun.FindAllStringSubmatch(line, -1) { + release, want := m[1], "make lint" + if path == "Makefile" { + inMakefile++ + if release == "$(GOLANGCI_LINT_VERSION)" || release == "${GOLANGCI_LINT_VERSION}" { + release = pin + } + want = "golangci-lint@$(GOLANGCI_LINT_VERSION)" + } + switch { + case m[1] == "": + problems = append(problems, fmt.Sprintf("%s:%d says %q, which runs whatever golangci-lint "+ + "release is installed, and .golangci.yml is in the v%s configuration format. Write %s", + path, i+1, m[0], format, want)) + case pin != "" && release != pin: + problems = append(problems, fmt.Sprintf("%s:%d runs golangci-lint %s, but the Makefile pins "+ + "%s for make lint. Write %s", path, i+1, release, pin, want)) + } + } + } + } + if inMakefile == 0 { + problems = append(problems, "the Makefile runs golangci-lint nowhere, so make lint runs no pinned release") + } + return problems, nil +} + +// TestGolangciLintProblems checks golangciLintProblems on configurations, +// Makefiles and docs. Each problem it finds has to hold the text given here, +// in order. +func TestGolangciLintProblems(t *testing.T) { + const ( + v1Config = "run:\n timeout: 5m\n" + v2Config = "version: \"2\"\nrun:\n timeout: 5m\n" + v1Makefile = "GOLANGCI_LINT_VERSION := v1.64.8\n\nlint:\n" + + "\tgo run github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) run ./...\n" + v2Makefile = "GOLANGCI_LINT_VERSION := v2.12.2\n\nlint:\n" + + "\tgo run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} run ./...\n" + ) + for _, c := range []struct { + name string + config string + files map[string]string + want []string + }{ + {"make lint in the docs", v1Config, + map[string]string{"Makefile": v1Makefile, "CONTRIBUTING.md": "# Run linter\nmake lint\n"}, nil}, + {"go run at the pinned release in the docs", v1Config, + map[string]string{"Makefile": v1Makefile, "CONTRIBUTING.md": "go run " + + "github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 run ./...\n"}, nil}, + {"installed golangci-lint in the Makefile", v1Config, + map[string]string{"Makefile": "lint:\n\tgolangci-lint run ./...\n"}, + []string{"sets GOLANGCI_LINT_VERSION to no release tag", `Makefile:2 says "golangci-lint run"`}}, + {"installed golangci-lint in the docs", v1Config, + map[string]string{"Makefile": v1Makefile, "docs/development.md": "# Run linter\ngolangci-lint run\n"}, + []string{`docs/development.md:2 says "golangci-lint run"`}}, + {"another release in the docs", v1Config, + map[string]string{"Makefile": v1Makefile, "CONTRIBUTING.md": "go run " + + "github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 run ./...\n"}, + []string{"CONTRIBUTING.md:1 runs golangci-lint v1.63.4, but the Makefile pins v1.64.8"}}, + {"v1 release for a v2 configuration", v2Config, + map[string]string{"Makefile": v1Makefile}, + []string{"pins golangci-lint v1.64.8, but .golangci.yml is in the v2 configuration format"}}, + {"v2 release for a v2 configuration", v2Config, + map[string]string{"Makefile": v2Makefile}, nil}, + {"unquoted version 2", "version: 2\n", + map[string]string{"Makefile": v2Makefile}, nil}, + {"no golangci-lint command in the Makefile", v1Config, + map[string]string{"Makefile": "GOLANGCI_LINT_VERSION := v1.64.8\n\nlint:\n\tgo vet ./...\n"}, + []string{"runs golangci-lint nowhere"}}, + } { + problems, err := golangciLintProblems(c.config, c.files) + if err != nil { + t.Fatalf("%s: %v", c.name, err) + } + ok := len(problems) == len(c.want) + for i := 0; ok && i < len(problems); i++ { + ok = strings.Contains(problems[i], c.want[i]) + } + if !ok { + t.Errorf("%s: golangciLintProblems = %q, want problems holding %q", c.name, problems, c.want) + } + } +} + +// TestDocsLintCommandRunsTheGolangciLintReleaseTheConfigIsWrittenFor covers the +// lint target in the Makefile, three lines of CONTRIBUTING.md and the setup +// steps in docs/development.md, which ran golangci-lint with whatever release +// was installed. .golangci.yml is in the v1 configuration format, which +// golangci-lint 2.12.2 stops on with "can't load config: unsupported version +// of the configuration" and exit code 3, while the CI lint job installs +// v1.64.8 and passes. +// +// The release the Makefile pins has to have the major version of the format of +// .golangci.yml: golangci-lint migrate writes version: "2" at the top of a v2 +// file, and v1.64.8 refuses a file that carries a version key. +func TestDocsLintCommandRunsTheGolangciLintReleaseTheConfigIsWrittenFor(t *testing.T) { + files := map[string]string{} + for _, root := range lintDocs { + err := filepath.WalkDir(filepath.Join(repoRoot, root), func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || (root != "Makefile" && !strings.HasSuffix(path, ".md")) { + return nil + } + rel, err := filepath.Rel(repoRoot, path) + if err != nil { + return err + } + rel = filepath.ToSlash(rel) + files[rel] = readRepoFile(t, rel) + return nil + }) + if err != nil { + t.Fatalf("walking %s: %v", root, err) + } + } + problems, err := golangciLintProblems(readRepoFile(t, ".golangci.yml"), files) + if err != nil { + t.Fatal(err) + } + for _, p := range problems { + t.Error(p) + } +} diff --git a/docs/development.md b/docs/development.md index 4e0ae2e..14bb79e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -23,7 +23,7 @@ go test -race ./... go build -o cryptodeps ./cmd/cryptodeps # Run linter -golangci-lint run +make lint ``` ## Adding Package Data