ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Run golangci-lint in Docker build

+139 -41
+10
.golangci.yml
··· 1 + version: "2" 2 + 3 + run: 4 + timeout: 5m 5 + tests: true 6 + 7 + linters: 8 + enable: 9 + - errcheck 10 + - staticcheck
+3
Dockerfile
··· 2 2 # Stage 1: build 3 3 FROM golang:1.24-alpine AS builder 4 4 WORKDIR /src 5 + ENV PATH="/go/bin:${PATH}" 5 6 # Enable module cache 6 7 COPY go.mod go.sum ./ 7 8 RUN go mod download 8 9 COPY . . 10 + RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.1.6 11 + RUN golangci-lint run ./... 9 12 RUN go test ./... -count=1 10 13 # Build static binary (no CGO) 11 14 RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o careme ./cmd/careme
+5 -2
cmd/careme/mail.go
··· 132 132 p.LastRecipes = append(p.LastRecipes, last.Title) 133 133 } 134 134 if err := rio.SaveParams(ctx, p); err != nil { 135 - if errors.Is(err, recipes.AlreadyExists) { 135 + if errors.Is(err, recipes.ErrAlreadyExists) { 136 136 slog.InfoContext(ctx, "params already exist, another process likely generated", "user", user.ID) 137 137 return 138 138 } ··· 154 154 } 155 155 156 156 var buf bytes.Buffer 157 - recipes.FormatMail(p, *shoppingList, &buf) 157 + if err := recipes.FormatMail(p, *shoppingList, &buf); err != nil { 158 + slog.ErrorContext(ctx, "failed to format mail", "error", err) 159 + return 160 + } 158 161 159 162 from := mail.NewEmail("Chef", "chef@careme.cooking") 160 163 subject := "Your new recipes are ready!"
+5 -1
cmd/careme/main.go
··· 55 55 if err != nil { 56 56 log.Fatalf("failed to create logsink: %v", err) 57 57 } 58 - defer closer.Close() 58 + defer func() { 59 + if err := closer.Close(); err != nil { 60 + slog.Error("failed to close logsink", "error", err) 61 + } 62 + }() 59 63 slog.SetDefault(slog.New(multi.Fanout(handler, slog.NewTextHandler(os.Stdout, nil)))) 60 64 // log.SetOutput(os.Stdout) // https://github.com/golang/go/issues/61892 61 65
+12 -4
cmd/careme/web.go
··· 48 48 mux.HandleFunc("/static/tailwind.css", func(w http.ResponseWriter, r *http.Request) { 49 49 w.Header().Set("Content-Type", "text/css; charset=utf-8") 50 50 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 51 - w.Write(tailwindCSS) 51 + if _, err := w.Write(tailwindCSS); err != nil { 52 + slog.ErrorContext(r.Context(), "failed to write tailwind css", "error", err) 53 + } 52 54 }) 53 55 54 56 locationserver, err := locations.New(context.TODO(), cfg) ··· 132 134 }) 133 135 134 136 mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) { 135 - w.Write([]byte("OK")) 137 + if _, err := w.Write([]byte("OK")); err != nil { 138 + slog.ErrorContext(r.Context(), "failed to write readiness response", "error", err) 139 + } 136 140 }) 137 141 138 142 mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { 139 143 w.Header().Set("Content-Type", "image/png") // <= without this, many UAs ignore it 140 144 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 141 - w.Write(favicon) 145 + if _, err := w.Write(favicon); err != nil { 146 + slog.ErrorContext(r.Context(), "failed to write favicon", "error", err) 147 + } 142 148 }) 143 149 144 150 server := &http.Server{ ··· 181 187 if err := svr.Shutdown(ctx); err != nil { 182 188 slog.Error("Server shutdown error", "error", err) 183 189 // Force close after timeout 184 - svr.Close() 190 + if closeErr := svr.Close(); closeErr != nil { 191 + slog.Error("Server close error", "error", closeErr) 192 + } 185 193 return err 186 194 } 187 195
+13 -3
cmd/careme/web_e2e_test.go
··· 139 139 if err != nil { 140 140 t.Fatalf("login request failed: %v", err) 141 141 } 142 - defer resp.Body.Close() 142 + defer func() { 143 + if err := resp.Body.Close(); err != nil { 144 + t.Fatalf("failed to close login response body: %v", err) 145 + } 146 + }() 143 147 if resp.StatusCode != http.StatusOK { 144 148 body := readAll(t, resp.Body) 145 149 t.Fatalf("expected login 200, got %d: %s", resp.StatusCode, body) ··· 158 162 func mustGetBody(t *testing.T, client *http.Client, url string) string { 159 163 t.Helper() 160 164 resp := mustGet(t, client, url) 161 - defer resp.Body.Close() 165 + defer func() { 166 + if err := resp.Body.Close(); err != nil { 167 + t.Fatalf("failed to close response body: %v", err) 168 + } 169 + }() 162 170 if resp.StatusCode != http.StatusOK { 163 171 body := readAll(t, resp.Body) 164 172 t.Fatalf("GET %s expected 200, got %d: %s", url, resp.StatusCode, body) ··· 179 187 resp := mustGet(t, client, current) 180 188 181 189 body := readAll(t, resp.Body) 182 - resp.Body.Close() 190 + if err := resp.Body.Close(); err != nil { 191 + t.Fatalf("failed to close response body: %v", err) 192 + } 183 193 184 194 if isSpinner(body) { 185 195 sawSpinner = true
+6 -1
internal/cache/file.go
··· 109 109 } 110 110 return err 111 111 } 112 - defer f.Close() 113 112 if _, err := f.WriteString(value); err != nil { 113 + if closeErr := f.Close(); closeErr != nil { 114 + return errors.Join(err, closeErr) 115 + } 116 + return err 117 + } 118 + if err := f.Close(); err != nil { 114 119 return err 115 120 } 116 121 return nil
+5 -1
internal/kroger/client.go
··· 80 80 if err != nil { 81 81 return "", err 82 82 } 83 - defer resp.Body.Close() 83 + defer func() { 84 + if err := resp.Body.Close(); err != nil { 85 + fmt.Printf("Kroger Response Close Error: %v\n", err) 86 + } 87 + }() 84 88 85 89 body, err := io.ReadAll(resp.Body) 86 90 if err != nil {
+2 -2
internal/locations/mock.go
··· 24 24 }, 25 25 } 26 26 27 - func (_ mock) GetLocationByID(ctx context.Context, locationID string) (*Location, error) { 27 + func (m mock) GetLocationByID(ctx context.Context, locationID string) (*Location, error) { 28 28 l, ok := fakes[locationID] 29 29 if !ok { 30 30 return nil, fmt.Errorf("no location %s", locationID) ··· 32 32 return &l, nil 33 33 } 34 34 35 - func (_ mock) GetLocationsByZip(ctx context.Context, zipcode string) ([]Location, error) { 35 + func (m mock) GetLocationsByZip(ctx context.Context, zipcode string) ([]Location, error) { 36 36 return lo.Values(fakes), nil 37 37 }
+5 -1
internal/logs/reader.go
··· 112 112 if err != nil { 113 113 return fmt.Errorf("failed to download blob: %w", err) 114 114 } 115 - defer resp.Body.Close() 115 + defer func() { 116 + if err := resp.Body.Close(); err != nil { 117 + slog.ErrorContext(ctx, "failed to close log blob stream", "error", err, "blob", blobName) 118 + } 119 + }() 116 120 117 121 _, err = io.Copy(w, resp.Body) 118 122 return err
+8 -6
internal/recipes/generator.go
··· 74 74 /*if len(p.Saved) > 0 { 75 75 instructions += " Enjoyed and saved :" 76 76 }*/ 77 - for _, saved := range p.Saved { 78 - // This ended up giving me a "Preference update + replacements requested" recipe 79 - // instructions += saved.Title + "; " //is this enough or do we keep the exact one? 80 - shoppingList.Recipes = append(shoppingList.Recipes, saved) 81 - } 77 + // This ended up giving me a "Preference update + replacements requested" recipe 78 + // instructions += saved.Title + "; " //is this enough or do we keep the exact one? 79 + shoppingList.Recipes = append(shoppingList.Recipes, p.Saved...) 82 80 83 81 slog.InfoContext(ctx, "regenerated chat", "location", p.String(), "duration", time.Since(start), "hash", hash) 84 82 return shoppingList, nil ··· 122 120 var ingredients []kroger.Ingredient 123 121 124 122 if ingredientblob, err := g.cache.Get(ctx, lochash); err == nil { 125 - defer ingredientblob.Close() 123 + defer func() { 124 + if err := ingredientblob.Close(); err != nil { 125 + slog.ErrorContext(ctx, "failed to close cached ingredients reader", "location", p.String(), "error", err) 126 + } 127 + }() 126 128 jsonReader := json.NewDecoder(ingredientblob) 127 129 if err := jsonReader.Decode(&ingredients); err == nil { 128 130 slog.Info("serving cached ingredients", "location", p.String(), "hash", lochash, "count", len(ingredients))
+12 -4
internal/recipes/io.go
··· 27 27 if err != nil { 28 28 return nil, err 29 29 } 30 - defer recipe.Close() 30 + defer func() { 31 + if err := recipe.Close(); err != nil { 32 + slog.ErrorContext(ctx, "failed to close cached recipe", "hash", hash, "error", err) 33 + } 34 + }() 31 35 32 36 var singleRecipe ai.Recipe 33 37 err = json.NewDecoder(recipe).Decode(&singleRecipe) ··· 42 46 if err != nil { 43 47 return nil, err 44 48 } 45 - defer shoppinglist.Close() 49 + defer func() { 50 + if err := shoppinglist.Close(); err != nil { 51 + slog.ErrorContext(ctx, "failed to close cached shopping list", "hash", hash, "error", err) 52 + } 53 + }() 46 54 47 55 var list ai.ShoppingList 48 56 err = json.NewDecoder(shoppinglist).Decode(&list) ··· 77 85 return errors.Join(errs...) 78 86 } 79 87 80 - var AlreadyExists = errors.New("already exists") 88 + var ErrAlreadyExists = errors.New("already exists") 81 89 82 90 func (rio *recipeio) SaveParams(ctx context.Context, p *generatorParams) error { 83 91 paramsJSON := lo.Must(json.Marshal(p)) 84 92 if err := rio.Cache.Put(ctx, p.Hash()+".params", string(paramsJSON), cache.IfNoneMatch()); err != nil { 85 93 if errors.Is(err, cache.ErrAlreadyExists) { 86 - return AlreadyExists 94 + return ErrAlreadyExists 87 95 } 88 96 slog.ErrorContext(ctx, "failed to cache params", "location", p.String(), "error", err) 89 97 return err
+7 -3
internal/recipes/io_test.go
··· 15 15 if err != nil { 16 16 t.Fatalf("failed to create temp dir: %v", err) 17 17 } 18 - defer os.RemoveAll(tmpDir) 18 + t.Cleanup(func() { 19 + if err := os.RemoveAll(tmpDir); err != nil { 20 + t.Fatalf("failed to remove temp dir: %v", err) 21 + } 22 + }) 19 23 20 24 rio := IO(cache.NewFileCache(tmpDir)) 21 25 p := DefaultParams(&locations.Location{ID: "123", Name: "Test Store"}, time.Date(2026, 1, 25, 0, 0, 0, 0, time.UTC)) ··· 39 43 switch { 40 44 case err == nil: 41 45 ok++ 42 - case errors.Is(err, AlreadyExists): 46 + case errors.Is(err, ErrAlreadyExists): 43 47 alreadyExists++ 44 48 default: 45 49 other++ ··· 47 51 } 48 52 49 53 if ok != 1 || other != 0 || alreadyExists != n-1 { 50 - t.Fatalf("expected 1 success + %d AlreadyExists, got ok=%d alreadyExists=%d other=%d", n-1, ok, alreadyExists, other) 54 + t.Fatalf("expected 1 success + %d ErrAlreadyExists, got ok=%d alreadyExists=%d other=%d", n-1, ok, alreadyExists, other) 51 55 } 52 56 }
+1 -1
internal/recipes/mock.go
··· 344 344 }, 345 345 } 346 346 347 - func (_ mock) GenerateRecipes(ctx context.Context, p *generatorParams) (*ai.ShoppingList, error) { 347 + func (m mock) GenerateRecipes(ctx context.Context, p *generatorParams) (*ai.ShoppingList, error) { 348 348 id := p.ConversationID 349 349 if id == "" { 350 350 id = uuid.NewString()
+5 -1
internal/recipes/params.go
··· 84 84 if err != nil { 85 85 return nil, fmt.Errorf("params not found for hash %s: %w", hash, err) 86 86 } 87 - defer paramsReader.Close() 87 + defer func() { 88 + if err := paramsReader.Close(); err != nil { 89 + slog.ErrorContext(ctx, "failed to close params reader", "hash", hash, "error", err) 90 + } 91 + }() 88 92 89 93 var params generatorParams 90 94 if err := json.NewDecoder(paramsReader).Decode(&params); err != nil {
+10 -5
internal/recipes/server.go
··· 132 132 } 133 133 } 134 134 s.Spin(w, r) 135 - return 136 135 } 137 136 138 137 func (s *server) handleRecipes(w http.ResponseWriter, r *http.Request) { ··· 153 152 return 154 153 } 155 154 156 - 157 155 p, err := loadParamsFromHash(ctx, hashParam, s.cache) 158 156 if err != nil { 159 157 slog.ErrorContext(ctx, "failed to load params for hash", "hash", hashParam, "error", err) ··· 161 159 return 162 160 } 163 161 if r.URL.Query().Get("mail") == "true" { 164 - FormatMail(p, *slist, w) 162 + if err := FormatMail(p, *slist, w); err != nil { 163 + slog.ErrorContext(ctx, "failed to render mail template", "error", err) 164 + http.Error(w, "failed to render mail template", http.StatusInternalServerError) 165 + } 165 166 return 166 167 } 167 168 FormatChatHTML(p, *slist, w) ··· 179 180 //if params are already saved redirect and assume someone kicks off genration 180 181 181 182 if err := s.SaveParams(ctx, p); err != nil { 182 - if errors.Is(err, AlreadyExists) { 183 + if errors.Is(err, ErrAlreadyExists) { 183 184 slog.InfoContext(ctx, "params already existed redirecting", "hash", p.Hash()) 184 185 redirectToHash(w, r, p.Hash(), false /*useStart*/) 185 186 return ··· 389 390 return 390 391 } 391 392 slog.Info("serving cached ingredients", "location", p.String(), "hash", lochash) 392 - defer ingredientblob.Close() 393 + defer func() { 394 + if err := ingredientblob.Close(); err != nil { 395 + slog.ErrorContext(ctx, "failed to close cached ingredients", "location", p.String(), "error", err) 396 + } 397 + }() 393 398 dec := json.NewDecoder(ingredientblob) 394 399 var ingredients []kroger.Ingredient 395 400 err = dec.Decode(&ingredients)
+5 -1
internal/recipes/storage_test.go
··· 15 15 if err != nil { 16 16 t.Fatalf("failed to create temp dir: %v", err) 17 17 } 18 - defer os.RemoveAll(tmpDir) 18 + t.Cleanup(func() { 19 + if err := os.RemoveAll(tmpDir); err != nil { 20 + t.Fatalf("failed to remove temp dir: %v", err) 21 + } 22 + }) 19 23 20 24 fileCache := cache.NewFileCache(tmpDir) 21 25
+15 -3
internal/recipes/user_profile_test.go
··· 17 17 if err != nil { 18 18 t.Fatalf("failed to create temp dir: %v", err) 19 19 } 20 - defer os.RemoveAll(tmpDir) 20 + t.Cleanup(func() { 21 + if err := os.RemoveAll(tmpDir); err != nil { 22 + t.Fatalf("failed to remove temp dir: %v", err) 23 + } 24 + }) 21 25 22 26 tmpCache := cache.NewFileCache(tmpDir) 23 27 storage := users.NewStorage(tmpCache) ··· 84 88 if err != nil { 85 89 t.Fatalf("failed to create temp dir: %v", err) 86 90 } 87 - defer os.RemoveAll(tmpDir) 91 + t.Cleanup(func() { 92 + if err := os.RemoveAll(tmpDir); err != nil { 93 + t.Fatalf("failed to remove temp dir: %v", err) 94 + } 95 + }) 88 96 89 97 tmpCache := cache.NewFileCache(tmpDir) 90 98 storage := users.NewStorage(tmpCache) ··· 156 164 if err != nil { 157 165 t.Fatalf("failed to create temp dir: %v", err) 158 166 } 159 - defer os.RemoveAll(tmpDir) 167 + t.Cleanup(func() { 168 + if err := os.RemoveAll(tmpDir); err != nil { 169 + t.Fatalf("failed to remove temp dir: %v", err) 170 + } 171 + }) 160 172 161 173 tmpCache := cache.NewFileCache(tmpDir) 162 174 storage := users.NewStorage(tmpCache)
+10 -2
internal/users/storage.go
··· 120 120 } 121 121 return nil, err 122 122 } 123 - defer userBytes.Close() 123 + defer func() { 124 + if err := userBytes.Close(); err != nil { 125 + slog.Error("failed to close user reader", "error", err, "user_id", id) 126 + } 127 + }() 124 128 decoder := json.NewDecoder(userBytes) 125 129 126 130 var user User ··· 139 143 } 140 144 return nil, err 141 145 } 142 - defer id.Close() 146 + defer func() { 147 + if err := id.Close(); err != nil { 148 + slog.Error("failed to close user email reader", "error", err, "email", normalized) 149 + } 150 + }() 143 151 data, err := io.ReadAll(id) 144 152 if err != nil { 145 153 return nil, fmt.Errorf("failed to read user ID: %w", err)