diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 90ded98..6011a79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,6 +32,11 @@ jobs: - name: Run Go Vet run: go vet ./... + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.59 + - name: Run Go Unit Tests run: go test -v ./... diff --git a/Makefile b/Makefile index 809f658..9fd0602 100644 --- a/Makefile +++ b/Makefile @@ -42,12 +42,16 @@ check: @chmod +x scripts/check.sh @./scripts/check.sh -## lint: Run gofmt and go vet +## lint: Run gofmt, go vet, and golangci-lint lint: @echo "==> Verifying Go formatting..." @test -z "$$(gofmt -l internal/ cmd/)" || (echo "Unformatted files:" && gofmt -l internal/ cmd/ && exit 1) @echo "==> Running Go vet..." @go vet ./... + @if command -v golangci-lint &> /dev/null; then \ + echo "==> Running golangci-lint..."; \ + golangci-lint run; \ + fi @echo "✓ Lint passed." ## format: Format all Go files with gofmt diff --git a/cmd/server/main.go b/cmd/server/main.go index 60906b4..955399c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -28,7 +28,9 @@ func main() { if err != nil { log.Fatalf("Fatal: Database initialization failed: %v", err) } - defer database.Close() + defer func() { + _ = database.Close() + }() log.Printf("==> Database connected: %s (WAL mode enabled)\n", cfg.DBPath) // 2. Initialize Auth Service & Bootstrap Admin diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 85cc02e..6f2a430 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -345,7 +345,9 @@ func (h *Handler) ImportOPML(w http.ResponseWriter, r *http.Request) { h.writeError(w, http.StatusBadRequest, "file field is required") return } - defer file.Close() + defer func() { + _ = file.Close() + }() data, err := io.ReadAll(file) if err != nil { @@ -524,7 +526,3 @@ func (h *Handler) ViewSharedEntry(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(html)) } - -type sqlNullHelper struct { - String string -} diff --git a/internal/api/server.go b/internal/api/server.go index c951268..eee9610 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -16,7 +16,6 @@ func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http r := chi.NewRouter() r.Use(middleware.RequestID) - r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Compress(5)) diff --git a/internal/crawler/crawler.go b/internal/crawler/crawler.go index 13c851f..f1ebd64 100644 --- a/internal/crawler/crawler.go +++ b/internal/crawler/crawler.go @@ -137,7 +137,7 @@ func (c *Crawler) runWorkerPool(ctx context.Context, feeds []models.Feed) { go func() { defer wg.Done() for f := range jobs { - c.fetchSingleFeed(ctx, f) + _ = c.fetchSingleFeed(ctx, f) } }() } diff --git a/internal/db/db.go b/internal/db/db.go index 6ed5180..1804c24 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -186,7 +186,9 @@ func (d *DB) ListCategories() ([]models.Category, error) { if err != nil { return nil, err } - defer rows.Close() + defer func() { + _ = rows.Close() + }() var categories []models.Category for rows.Next() { @@ -264,7 +266,9 @@ func (d *DB) ListFeeds() ([]models.Feed, error) { if err != nil { return nil, err } - defer rows.Close() + defer func() { + _ = rows.Close() + }() var feeds []models.Feed for rows.Next() { @@ -479,7 +483,9 @@ func (d *DB) UpsertFeedEntries(feedID string, entries []models.FeedEntry) (int, if err != nil { return 0, err } - defer stmt.Close() + defer func() { + _ = stmt.Close() + }() insertedCount := 0 for _, e := range entries { @@ -519,9 +525,10 @@ func (d *DB) ListEntries(f EntryFilter) ([]models.FeedEntry, int, error) { where := []string{"1=1"} args := []interface{}{} - if f.View == "inbox" { + switch f.View { + case "inbox": where = append(where, "e.is_read = 0") - } else if f.View == "starred" { + case "starred": where = append(where, "e.is_starred = 1") } @@ -596,7 +603,9 @@ func (d *DB) ListEntries(f EntryFilter) ([]models.FeedEntry, int, error) { if err != nil { return nil, 0, err } - defer rows.Close() + defer func() { + _ = rows.Close() + }() var entries []models.FeedEntry for rows.Next() { diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 1e7200e..57b3dff 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -14,14 +14,18 @@ func TestDatabaseOperations(t *testing.T) { if err != nil { t.Fatalf("failed to create temp dir: %v", err) } - defer os.RemoveAll(tempDir) + defer func() { + _ = os.RemoveAll(tempDir) + }() dbPath := filepath.Join(tempDir, "test.db") database, err := InitDB(dbPath) if err != nil { t.Fatalf("InitDB failed: %v", err) } - defer database.Close() + defer func() { + _ = database.Close() + }() // 1. Test User u, err := database.CreateUser("admin", "$2a$10$dummyhashvalue") diff --git a/internal/parser/parser.go b/internal/parser/parser.go index ccb0cf4..ea19aca 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -53,7 +53,9 @@ func (p *Parser) FetchFeed(ctx context.Context, feedURL, etag, lastModified stri if err != nil { return nil, fmt.Errorf("failed to fetch url: %w", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode == http.StatusNotModified { return &FetchResult{NotModified: true}, nil diff --git a/scripts/check.sh b/scripts/check.sh index 0cbd4fe..679ec2e 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -15,9 +15,14 @@ if [ -n "$UNFORMATTED" ]; then fi echo "✓ Go files formatted correctly." -echo "==> 2. Running Go vet..." +echo "==> 2. Running Go vet & golangci-lint..." go vet ./... -echo "✓ Go vet passed." +if command -v golangci-lint &> /dev/null; then + golangci-lint run + echo "✓ golangci-lint passed (0 issues)." +else + echo "⚠ golangci-lint not installed locally (will run in CI)." +fi echo "==> 3. Running Go tests..." go test -v ./...