Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends libsqlite3-dev

# golangci-lint-action@v6's "latest" resolves within the v1.x line
# (v1.64.8 at time of writing), which refuses to run against this
# module's go 1.26.5 directive ("the Go language version used to
# build golangci-lint is lower than the targeted Go version"). v9
# supports golangci-lint v2, and pinning a v2 release keeps the
# Go-version check satisfied without depending on how "latest"
# happens to resolve.
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
version: v2.12.2
args: --build-tags=sqlite_fts5

test:
Expand Down Expand Up @@ -225,14 +232,15 @@ jobs:
# set up — gives gosec's type-checking source loader what it needs to
# actually analyze cgo-dependent packages instead of skipping them.
#
# continue-on-error: a first run against this codebase turned up 61
# pre-existing findings (mostly G104 unhandled-error-on-Close, plus a
# handful of G301/G304/G306/G115/G202/G401/G505). None of that is this
# job's fault to gate on — report-only until it's triaged, then remove
# continue-on-error so new findings start blocking merges.
# The 61 pre-existing findings from the first run against this codebase
# (mostly G104 unhandled-error-on-Close, plus a handful of
# G301/G304/G306/G115/G202/G401/G505) have all been triaged: fixed where
# real, suppressed with an inline `// #nosec Gxxx -- reason` comment
# (gosec's own directive — it does not understand golangci-lint's
# `//nolint` comments when run standalone like this) where the flagged
# pattern is safe by inspection. New findings now block merges.
gosec:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -260,12 +268,15 @@ jobs:
# see, which is the flag NilAway's own docs call out as the standard way to
# keep a first run's signal-to-noise ratio sane on an existing codebase.
#
# continue-on-error: a first run against this codebase found 3 potential
# nil panics in indexer/indexer.go, pre-existing and unrelated to this
# workflow. Report-only until triaged, then remove continue-on-error.
# The 3 potential nil panics found by the first run against this codebase
# (all in indexer/indexer.go, one structural root cause) have been fixed:
# a nilable `var x []T` accumulator became a non-nil `make([]T, 0, n)`,
# and the two consuming loops were merged into the same block that
# assigns and validates the embedder's result slices, giving NilAway's
# flow analysis a straight-line path to see them as non-nil. New findings
# now block merges.
nilaway:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions cmd/codamigo/doctor_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func doctorCmd() *cli.Command {
if err != nil {
fmt.Printf("[FAIL] Store open error: %v\n", err)
} else {
defer s.Close()
defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way
stats, err := s.Stats(ctx)
if err != nil {
fmt.Printf("[FAIL] Stats error: %v\n", err)
Expand Down Expand Up @@ -157,7 +157,7 @@ func doctorCmd() *cli.Command {
if err != nil {
fmt.Printf("[FAIL] Walker error: %v\n", err)
} else {
defer w.Close()
defer func() { _ = w.Close() }() // best-effort cleanup; the process is exiting either way
count := 0
errCount := 0
for _, err := range w.Walk(ctx) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/codamigo/graph_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func runGraphQuery(
if err != nil {
return err
}
defer s.Close()
defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way

refs, err := run(query.New(emb, s), symbol)
if err != nil {
Expand Down
18 changes: 11 additions & 7 deletions cmd/codamigo/init_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func initCmd() *cli.Command {
}

// Write global config.
if err := os.MkdirAll(filepath.Dir(globalPath), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(globalPath), 0o750); err != nil {
return fmt.Errorf("creating config directory: %w", err)
}
// 0o600 below matters: this file may hold an API key or a
Expand Down Expand Up @@ -111,11 +111,11 @@ func initCmd() *cli.Command {
// Always create project config if missing — runs regardless of global config state.
projectPath := config.ProjectConfigPath()
if _, err := os.Stat(projectPath); errors.Is(err, fs.ErrNotExist) {
if err := os.MkdirAll(filepath.Dir(projectPath), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(projectPath), 0o750); err != nil {
return fmt.Errorf("creating project config directory: %w", err)
}
projectContent := "# codamigo project settings\n# include_patterns: []\n# exclude_patterns: []\n"
if err := os.WriteFile(projectPath, []byte(projectContent), 0o644); err != nil {
if err := os.WriteFile(projectPath, []byte(projectContent), 0o600); err != nil {
return fmt.Errorf("writing project config: %w", err)
}
fmt.Printf("Created project config: %s\n", projectPath)
Expand All @@ -134,11 +134,11 @@ func initCmd() *cli.Command {
dataDir, err := config.ProjectDataDir(wd)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not resolve data directory: %v\n", err)
} else if err := os.MkdirAll(dataDir, 0o755); err != nil {
} else if err := os.MkdirAll(dataDir, 0o750); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not create data directory: %v\n", err)
} else {
pathFile := filepath.Join(dataDir, "project_path")
if err := os.WriteFile(pathFile, []byte(wd), 0o644); err != nil {
if err := os.WriteFile(pathFile, []byte(wd), 0o600); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not write project_path: %v\n", err)
}
}
Expand Down Expand Up @@ -201,9 +201,9 @@ func initCmd() *cli.Command {
// buffered input from piped stdin is not lost between calls.
func readPrompt(scanner *bufio.Scanner, w io.Writer, prompt, defaultVal string) string {
if defaultVal != "" {
fmt.Fprintf(w, "%s [%s]: ", prompt, defaultVal)
_, _ = fmt.Fprintf(w, "%s [%s]: ", prompt, defaultVal)
} else {
fmt.Fprintf(w, "%s: ", prompt)
_, _ = fmt.Fprintf(w, "%s: ", prompt)
}
if scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
Expand All @@ -227,6 +227,7 @@ func appendToGitignore(projectRoot string) error {
const entry = ".codamigo/"
gitignorePath := filepath.Join(projectRoot, ".gitignore")

// #nosec G304 -- gitignorePath is derived from the CLI's own project root, not external input
content, err := os.ReadFile(gitignorePath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("reading .gitignore: %w", err)
Expand All @@ -238,6 +239,9 @@ func appendToGitignore(projectRoot string) error {
}
}

// #nosec G304,G302 -- 0o644 matches .gitignore's conventional world-readable
// permissions; gitignorePath is derived from the CLI's own project root,
// not external input.
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("opening .gitignore: %w", err)
Expand Down
12 changes: 6 additions & 6 deletions cmd/codamigo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ func indexCmd() *cli.Command {
if err != nil {
return err
}
defer s.Close()
defer w.Close() //nolint:errcheck
defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way
defer func() { _ = w.Close() }() // best-effort cleanup; the process is exiting either way

// Wrap ctx so the TUI can cancel the indexer directly via ctrl+c.
// In TTY mode the terminal is in raw mode (ISIG cleared), so ctrl+c
Expand Down Expand Up @@ -233,8 +233,8 @@ func serveCmd() *cli.Command {
if err != nil {
return err
}
defer s.Close()
defer w.Close()
defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way
defer func() { _ = w.Close() }() // best-effort cleanup; the process is exiting either way
queryEmb, err := queryEmbedderFor(cfg, indexEmb)
if err != nil {
return fmt.Errorf("creating embedder: %w", err)
Expand All @@ -251,7 +251,7 @@ func serveCmd() *cli.Command {
if err != nil {
return fmt.Errorf("creating watcher: %w", err)
}
defer wch.Close()
defer func() { _ = wch.Close() }() // best-effort cleanup; the process is exiting either way
srv := mcp.NewServer(q, idx, wch,
mcp.WithNonCodeLanguages(cfg.NonCodeLanguages),
mcp.WithGraph(cfg.GraphEnabled()),
Expand Down Expand Up @@ -422,7 +422,7 @@ func buildComponents(cfg *config.Config, storePath string, dim int) (*chunker.Ch
filter := buildExtensionFilter(allLangs)
w, err := walker.New(cfg.ProjectRoot, cfg, walker.WithFileFilter(filter))
if err != nil {
s.Close()
_ = s.Close() // best-effort cleanup; the walker error below is the one worth reporting
return nil, nil, nil, fmt.Errorf("creating walker: %w", err)
}
return c, s, w, nil
Expand Down
2 changes: 1 addition & 1 deletion cmd/codamigo/map_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func mapCmd() *cli.Command {
if err != nil {
return err
}
defer s.Close()
defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way

q := query.New(emb, s)

Expand Down
8 changes: 4 additions & 4 deletions cmd/codamigo/reset_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,19 @@ func resetCmd() *cli.Command {
// is testable without touching os.Stdin/os.Stdout.
func runReset(storePath string, force bool, in io.Reader, out io.Writer) error {
if _, err := os.Stat(storePath); errors.Is(err, fs.ErrNotExist) {
fmt.Fprintln(out, "Nothing to reset.")
_, _ = fmt.Fprintln(out, "Nothing to reset.")
return nil
}

if !force {
fmt.Fprintf(out, "The following file will be deleted:\n %s\nDelete store file? [y/N]: ", storePath)
_, _ = fmt.Fprintf(out, "The following file will be deleted:\n %s\nDelete store file? [y/N]: ", storePath)
scanner := bufio.NewScanner(in)
answer := ""
if scanner.Scan() {
answer = strings.TrimSpace(scanner.Text())
}
if !strings.EqualFold(answer, "y") {
fmt.Fprintln(out, "Aborted.")
_, _ = fmt.Fprintln(out, "Aborted.")
return nil
}
}
Expand All @@ -66,6 +66,6 @@ func runReset(storePath string, force bool, in io.Reader, out io.Writer) error {
return fmt.Errorf("deleting store%s: %w", suffix, err)
}
}
fmt.Fprintf(out, "Store deleted: %s\n", storePath)
_, _ = fmt.Fprintf(out, "Store deleted: %s\n", storePath)
return nil
}
2 changes: 1 addition & 1 deletion cmd/codamigo/search_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func searchCmd() *cli.Command {
if err != nil {
return err
}
defer s.Close()
defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way

maxTokens := cmd.Int("max-tokens")
if maxTokens < 0 {
Expand Down
8 changes: 6 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
package config

import (
"crypto/sha1"
"crypto/sha1" // #nosec G505 -- non-cryptographic use, see ProjectHash
"encoding/hex"
"errors"
"fmt"
Expand Down Expand Up @@ -307,6 +307,9 @@ func ProjectConfigPath() string {
// that "/home/user/project/" and "/home/user/project" produce the same hash.
func ProjectHash(projectRoot string) string {
trimmed := strings.Trim(projectRoot, "/")
// #nosec G401 -- non-cryptographic use: stable directory-name hash, not a
// security boundary; switching algorithms would relocate existing users'
// data dirs.
sum := sha1.Sum([]byte(trimmed))
return hex.EncodeToString(sum[:])
}
Expand Down Expand Up @@ -365,11 +368,12 @@ func HomeProjectConfigPath(projectRoot string) (string, error) {
// Returns an error if the file does not exist, contains unknown keys, or has
// malformed duration strings.
func Load(path string) (*Config, error) {
// #nosec G304 -- path is the CLI's own config path (flag/default), not external input
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening config %q: %w", path, err)
}
defer f.Close()
defer func() { _ = f.Close() }() // best-effort cleanup; the file is only being read

dec := yaml.NewDecoder(f)
dec.KnownFields(true)
Expand Down
16 changes: 11 additions & 5 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ func writeTempConfig(t *testing.T, content string) string {
if _, err := f.WriteString(content); err != nil {
t.Fatalf("writing temp config: %v", err)
}
f.Close()
if err := f.Close(); err != nil {
t.Fatalf("closing temp config: %v", err)
}
return f.Name()
}

Expand Down Expand Up @@ -326,9 +328,13 @@ func ExampleLoad() {
if err != nil {
panic(err)
}
defer os.Remove(f.Name())
f.WriteString("embedding_model: my-model\npoll_interval: 10s\n")
f.Close()
defer func() { _ = os.Remove(f.Name()) }()
if _, err := f.WriteString("embedding_model: my-model\npoll_interval: 10s\n"); err != nil {
panic(err)
}
if err := f.Close(); err != nil {
panic(err)
}

cfg, err := config.Load(f.Name())
if err != nil {
Expand Down Expand Up @@ -591,7 +597,7 @@ func TestProjectHash(t *testing.T) {
t.Errorf("hash length = %d, want 40", len(h))
}
for _, c := range h {
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
t.Errorf("hash contains non-hex char %q in %q", c, h)
}
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/text v0.39.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
Expand Down
4 changes: 2 additions & 2 deletions indexer/graph_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func graphFixture(t *testing.T, src string, opts ...indexer.Option) store.Store
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close() })
t.Cleanup(func() { _ = s.Close() })

root := t.TempDir()
if err = os.WriteFile(filepath.Join(root, "main.go"), []byte(src), 0o644); err != nil {
Expand Down Expand Up @@ -159,7 +159,7 @@ func TestIndex_ReindexReplacesGraph(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer s.Close()
defer func() { _ = s.Close() }()

root := t.TempDir()
path := filepath.Join(root, "main.go")
Expand Down
Loading
Loading