Skip to content

Commit 4118e88

Browse files
wesmclaude
andauthored
Enforce golangci-lint, fix lint warnings, harden CI (#281)
## Summary - Add `.golangci.yml` with errcheck, govet, ineffassign, staticcheck, unused linters - Make CI lint step blocking and pin golangci-lint to v1.64.8 - Pin all GitHub Actions to immutable commit SHAs; add Dependabot for weekly updates - Add `make lint` target and `make install-hooks` (copies pre-commit hook into `.git/hooks/`, works in linked worktrees) - Fix all existing lint warnings: unchecked errors on writes, JSON encoding, rollbacks, `os.Remove`/`os.Rename`, `Chdir` - Replace `fmt.Sscanf` with `strconv` for numeric parsing; handle `+meta` build metadata in semver comparison - Stop sync loop when progress stream write fails (disconnected client) - Remove unused `assertArgsOrder` test helper ## Test plan - [x] `go test ./...` passes - [x] `make lint` passes with no warnings - [x] CI lint job passes (now blocking) - [x] `make install-hooks` works in normal repos and linked worktrees 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 81eb103 commit 4118e88

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+979
-870
lines changed

.githooks/pre-commit

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
if ! command -v golangci-lint >/dev/null 2>&1; then
5+
echo "golangci-lint is required for local commit checks." >&2
6+
echo "Install with: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1" >&2
7+
exit 1
8+
fi
9+
10+
golangci-lint run ./...

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: github-actions
4+
directory: /
5+
schedule:
6+
interval: weekly
7+
- package-ecosystem: gomod
8+
directory: /
9+
schedule:
10+
interval: weekly

.github/workflows/ci.yml

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ jobs:
1818
os: [ubuntu-latest, macos-latest, windows-latest]
1919
runs-on: ${{ matrix.os }}
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2222

23-
- uses: actions/setup-go@v5
23+
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
2424
with:
2525
go-version: '1.24'
2626

@@ -64,9 +64,9 @@ jobs:
6464
coverage:
6565
runs-on: ubuntu-latest
6666
steps:
67-
- uses: actions/checkout@v4
67+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
6868

69-
- uses: actions/setup-go@v5
69+
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
7070
with:
7171
go-version: '1.24'
7272

@@ -76,7 +76,7 @@ jobs:
7676
run: go test -race -tags integration -p 1 -coverprofile=coverage.out ./...
7777

7878
- name: Upload coverage
79-
uses: codecov/codecov-action@v4
79+
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
8080
with:
8181
file: coverage.out
8282
continue-on-error: true
@@ -98,9 +98,9 @@ jobs:
9898
--health-timeout 5s
9999
--health-retries 5
100100
steps:
101-
- uses: actions/checkout@v4
101+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
102102

103-
- uses: actions/setup-go@v5
103+
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
104104
with:
105105
go-version: '1.24'
106106

@@ -112,24 +112,23 @@ jobs:
112112
lint:
113113
runs-on: ubuntu-latest
114114
steps:
115-
- uses: actions/checkout@v4
115+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
116116

117-
- uses: actions/setup-go@v5
117+
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
118118
with:
119119
go-version: '1.24'
120120

121121
- name: golangci-lint
122-
uses: golangci/golangci-lint-action@v4
122+
uses: golangci/golangci-lint-action@9fae48acfc02a90574d7c304a1758ef9895495fa # v7
123123
with:
124-
version: latest
125-
continue-on-error: true
124+
version: v2.10.1
126125

127126
nix:
128127
runs-on: ubuntu-latest
129128
steps:
130-
- uses: actions/checkout@v4
129+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
131130

132-
- uses: cachix/install-nix-action@v27
131+
- uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
133132
with:
134133
nix_path: nixpkgs=channel:nixos-unstable
135134

.golangci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
version: "2"
2+
run:
3+
tests: true
4+
linters:
5+
default: none
6+
enable:
7+
- errcheck
8+
- govet
9+
- ineffassign
10+
- modernize
11+
- staticcheck
12+
- unused
13+
settings:
14+
errcheck:
15+
check-type-assertions: false
16+
check-blank: false
17+
exclusions:
18+
generated: lax
19+
presets:
20+
- comments
21+
- common-false-positives
22+
- legacy
23+
- std-error-handling
24+
rules:
25+
- linters:
26+
- errcheck
27+
path: _test\.go
28+
- linters:
29+
- modernize
30+
text: "omitzero:"
31+
paths:
32+
- third_party$
33+
- builtin$
34+
- examples$
35+
issues:
36+
max-issues-per-linter: 0
37+
max-same-issues: 0
38+
formatters:
39+
exclusions:
40+
generated: lax
41+
paths:
42+
- third_party$
43+
- builtin$
44+
- examples$

.roborev.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ terminal is local-only data that does not cross a trust boundary. Do not flag:
3535
- "unbounded capture" of git command stderr (git stderr is practically bounded)
3636
These are the same class of local-data-in-local-tool issues covered above.
3737
38+
The repository tracks hook templates in .githooks/ as reference scripts. The
39+
install-hooks target copies these into .git/hooks/ (untracked) so the installed
40+
hook is frozen at install time and unaffected by branch switches. The hook runs
41+
golangci-lint directly, not via make, to avoid delegation to tracked files. Do
42+
not flag the tracked .githooks/ template directory as a supply-chain risk — it
43+
is a source template, not an active hook path.
44+
3845
Review outputs stored in the local database are generated by the user's own
3946
locally-configured AI agents. When these outputs are re-embedded into subsequent
4047
prompts (e.g., compact consolidation re-verifying prior findings), this is

Makefile

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
44
LDFLAGS := -X github.com/roborev-dev/roborev/internal/version.Version=$(VERSION)
55

6-
.PHONY: build install clean test test-integration test-postgres test-all postgres-up postgres-down test-postgres-ci
6+
.PHONY: build install clean test test-integration test-postgres test-all postgres-up postgres-down test-postgres-ci lint install-hooks
77

88
build:
99
@mkdir -p bin
@@ -45,6 +45,25 @@ test-postgres: postgres-up
4545
# Run all tests (unit + integration + postgres)
4646
test-all: test-integration test-postgres
4747

48+
# Lint Go code with project defaults
49+
lint:
50+
@if ! command -v golangci-lint >/dev/null 2>&1; then \
51+
echo "golangci-lint not found. Install with: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1" >&2; \
52+
exit 1; \
53+
fi
54+
golangci-lint run ./...
55+
56+
# Install pre-commit hook, resolving the hooks directory via git so
57+
# this works in both normal repos and linked worktrees
58+
install-hooks:
59+
@hooks_rel=$$(git rev-parse --git-path hooks) && \
60+
hooks_dir=$$(cd "$$(dirname "$$hooks_rel")" && echo "$$PWD/$$(basename "$$hooks_rel")") && \
61+
git config --local core.hooksPath "$$hooks_dir" && \
62+
mkdir -p "$$hooks_dir" && \
63+
cp .githooks/pre-commit "$$hooks_dir/pre-commit" && \
64+
chmod +x "$$hooks_dir/pre-commit" && \
65+
echo "Installed pre-commit hook to $$hooks_dir/pre-commit"
66+
4867
# CI target: run postgres tests without managing docker (assumes postgres is running)
4968
test-postgres-ci:
5069
go test -tags=postgres -v ./internal/storage/... -run Integration

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ Full documentation available at **[roborev.io](https://roborev.io)**:
227227
git clone https://github.com/roborev-dev/roborev
228228
cd roborev
229229
go test ./...
230+
make lint # run full static lint checks locally
230231
make install
232+
make install-hooks # install pre-commit hook to run lint before commit
231233
```
232234

233235
## License

cmd/roborev/analyze.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -470,10 +470,10 @@ func runPerFileAnalysis(cmd *cobra.Command, repoRoot string, analysisType *analy
470470
func buildOutputPrefix(analysisType string, filePaths []string) string {
471471
sort.Strings(filePaths)
472472
var sb strings.Builder
473-
sb.WriteString(fmt.Sprintf("## %s Analysis\n\n", analysisType))
473+
fmt.Fprintf(&sb, "## %s Analysis\n\n", analysisType)
474474
sb.WriteString("**Files:**\n")
475475
for _, path := range filePaths {
476-
sb.WriteString(fmt.Sprintf("- %s\n", path))
476+
fmt.Fprintf(&sb, "- %s\n", path)
477477
}
478478
sb.WriteString("\n---\n\n")
479479
return sb.String()
@@ -485,7 +485,7 @@ func enqueueAnalysisJob(repoRoot, prompt, outputPrefix, label string, opts analy
485485
if opts.branch != "" && opts.branch != "HEAD" {
486486
branch = opts.branch
487487
}
488-
reqBody, _ := json.Marshal(map[string]interface{}{
488+
reqBody, _ := json.Marshal(map[string]any{
489489
"repo_path": repoRoot,
490490
"git_ref": label, // Use analysis type name as the TUI label
491491
"branch": branch,
@@ -729,10 +729,7 @@ func waitForAnalysisJob(ctx context.Context, serverAddr string, jobID int64) (*s
729729
}
730730

731731
if pollInterval < maxInterval {
732-
pollInterval = pollInterval * 3 / 2
733-
if pollInterval > maxInterval {
734-
pollInterval = maxInterval
735-
}
732+
pollInterval = min(pollInterval*3/2, maxInterval)
736733
}
737734
}
738735
}
@@ -741,7 +738,7 @@ func waitForAnalysisJob(ctx context.Context, serverAddr string, jobID int64) (*s
741738
func buildFixPrompt(analysisType *analyze.AnalysisType, analysisOutput string) string {
742739
var sb strings.Builder
743740
sb.WriteString("# Fix Request\n\n")
744-
sb.WriteString(fmt.Sprintf("An analysis of type **%s** was performed and produced the following findings:\n\n", analysisType.Name))
741+
fmt.Fprintf(&sb, "An analysis of type **%s** was performed and produced the following findings:\n\n", analysisType.Name)
745742
sb.WriteString("## Analysis Findings\n\n")
746743
sb.WriteString(analysisOutput)
747744
sb.WriteString("\n\n## Instructions\n\n")
@@ -765,7 +762,7 @@ func buildCommitPrompt(analysisType *analyze.AnalysisType) string {
765762
sb.WriteString("2. Stage the appropriate files\n")
766763
sb.WriteString("3. Create a git commit with a descriptive message\n\n")
767764
sb.WriteString("The commit message should:\n")
768-
sb.WriteString(fmt.Sprintf("- Reference the '%s' analysis that prompted the changes\n", analysisType.Name))
765+
fmt.Fprintf(&sb, "- Reference the '%s' analysis that prompted the changes\n", analysisType.Name)
769766
sb.WriteString("- Summarize what was changed and why\n")
770767
sb.WriteString("- Be concise but informative\n")
771768
return sb.String()
@@ -833,7 +830,7 @@ func runFixAgent(cmd *cobra.Command, repoPath, agentName, model, reasoning, prom
833830

834831
// markJobAddressed marks a job as addressed via the API
835832
func markJobAddressed(serverAddr string, jobID int64) error {
836-
reqBody, _ := json.Marshal(map[string]interface{}{
833+
reqBody, _ := json.Marshal(map[string]any{
837834
"job_id": jobID,
838835
"addressed": true,
839836
})

cmd/roborev/analyze_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ func TestWaitForAnalysisJob_Timeout(t *testing.T) {
326326
ID: 42,
327327
Status: storage.JobStatusQueued,
328328
}
329-
_ = json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []storage.ReviewJob{job}})
329+
_ = json.NewEncoder(w).Encode(map[string]any{"jobs": []storage.ReviewJob{job}})
330330
}))
331331
defer ts.Close()
332332

@@ -366,7 +366,7 @@ func TestMarkJobAddressed(t *testing.T) {
366366
t.Errorf("unexpected method: %s", r.Method)
367367
}
368368

369-
var req map[string]interface{}
369+
var req map[string]any
370370
_ = json.NewDecoder(r.Body).Decode(&req)
371371
gotJobID = int64(req["job_id"].(float64))
372372
gotAddressed = req["addressed"].(bool)
@@ -501,9 +501,9 @@ func TestShowAnalysisPrompt(t *testing.T) {
501501

502502
// Verify there's substantial content after the template header
503503
// (the prompt templates are all multi-line with instructions)
504-
idx := strings.Index(outputStr, "## Prompt Template")
505-
if idx >= 0 {
506-
afterHeader := outputStr[idx+len("## Prompt Template"):]
504+
_, after, ok := strings.Cut(outputStr, "## Prompt Template")
505+
if ok {
506+
afterHeader := after
507507
if len(strings.TrimSpace(afterHeader)) < 50 {
508508
t.Error("prompt template section should have substantial content")
509509
}
@@ -573,7 +573,7 @@ func TestEnqueueAnalysisJob(t *testing.T) {
573573
ts, _ := newMockServer(t, MockServerOpts{
574574
JobIDStart: 42,
575575
OnEnqueue: func(w http.ResponseWriter, r *http.Request) {
576-
var req map[string]interface{}
576+
var req map[string]any
577577
_ = json.NewDecoder(r.Body).Decode(&req)
578578

579579
if req["agentic"] != true {
@@ -614,7 +614,7 @@ func TestEnqueueAnalysisJobBranchName(t *testing.T) {
614614
var branch string
615615
ts, _ := newMockServer(t, MockServerOpts{
616616
OnEnqueue: func(w http.ResponseWriter, r *http.Request) {
617-
var req map[string]interface{}
617+
var req map[string]any
618618
_ = json.NewDecoder(r.Body).Decode(&req)
619619
branch, _ = req["branch"].(string)
620620
w.WriteHeader(http.StatusCreated)

0 commit comments

Comments
 (0)