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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Install golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.11.3
version: v2.12.2
args: --help

- name: Quality - formatting
Expand Down
130 changes: 119 additions & 11 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,128 @@
version: "2"

run:
timeout: 3m
go: "1.26.1"

linters:
# These are enabled by default in v2, listed explicitly for clarity.
# Note: gosimple is merged into staticcheck in golangci-lint v2.
enable:
- govet
- errcheck
- staticcheck
- unused
- ineffassign
# Defaults — golangci-lint's standard set; the correctness backbone
- errcheck # Report unchecked errors (the #1 Go bug source)
- govet # Stdlib vet suite: printf, copylocks, struct tags, …
- ineffassign # Detect assignments whose value is never used
- staticcheck # Large bundle of correctness + simplification checks
- unused # Find unused code (funcs, vars, types, consts)

# Complexity and test-discipline linters
- cyclop # Cap per-function cyclomatic complexity (max in settings)
- gocritic # Opinionated correctness/style/perf diagnostics
- tparallel # Catch misuse of t.Parallel() (defer-vs-Cleanup, setup races)
- testifylint # Enforce correct testify usage (assert/require, arg order)
- thelper # Require t.Helper() in test helpers (buildXxx, newXxxServer)
- usetesting # Prefer t.Context()/t.Setenv() over context/os equivalents

# HTTP/Context linters
- bodyclose # Checks whether HTTP response body is closed
- noctx # Find HTTP requests without context.Context

# Error handling linters
- errorlint # Find code that will cause problems with error wrapping
- nilerr # Find code that returns nil even if it checks error is not nil

# Code quality linters
- misspell # Check for misspelled English words
- unconvert # Remove unnecessary type conversions
- unparam # Report unused function parameters
- wastedassign # Find wasted assignment statements

# Security linter
- gosec # Security checker

# Context and duration linters
- contextcheck # Check function context parameter usage
- durationcheck # Check for two durations multiplied together

# Naming and completeness linters
- errname # Check error type naming conventions
- predeclared # Find shadowing of Go's predeclared identifiers

settings:
cyclop:
# A backstop, not a style gate: today's worst function is 23, so this
# allows current code but trips on anything growing meaningfully worse.
max-complexity: 25

errcheck:
check-type-assertions: true
check-blank: false # Don't check explicitly ignored errors with _
exclude-functions:
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
- (*os.File).Close
- (net/http.ResponseWriter).Write

govet:
enable-all: true
disable:
- fieldalignment # Too noisy for minimal gains
- shadow # Too noisy - variable shadowing is common in Go

errorlint:
asserts: false
comparison: false

gosec:
excludes:
- G101 # Hardcoded credentials — false positives on *SecretsBasePath /
# *KeysBasePath URL constants; tokens come from keyring/env/flag,
# never hardcoded.
- G104 # Unhandled errors (covered by errcheck)
- G115 # Integer overflow conversion — standard Go pattern
- G204 # Subprocess with variable — expected in CLI tools
- G301 # Directory permissions 0755 — standard for user config dirs
- G304 # File path provided as taint input — common in CLI tools
- G306 # WriteFile permissions 0644 — fine for non-secret files
- G703 # Path traversal via taint — same class as G304; paths derive
# from the user's own home/config dir, filenames are constants.

misspell:
locale: US

staticcheck:
checks:
- all
- -SA1019 # Ignore deprecation warnings (handle separately)
- -ST1000 # Package comments — low value for internal CLI

testifylint:
disable:
# Tests assert exact JSON-decoded whole numbers (IDs, byte sizes, counts)
# which decode as float64; InEpsilon/InDelta would weaken these identity
# checks. No test compares results of actual float arithmetic.
- float-compare

exclusions:
rules:
# gosec false-positives on test fixtures (e.g. hardcoded fake tokens);
# cyclop is noisy on table/dispatch test servers (newSiteTestServer is 33);
# bodyclose findings in tests are synthetic NopCloser bodies or nil-response
# false positives (the client returns nil on error) — production code is clean.
# errcheck stays on for tests — unchecked errors there can mask bugs.
- path: _test\.go
linters:
- gosec
- cyclop
- bodyclose
# unparam flags the uniform test-helper signatures STYLE.md mandates
# (buildXxxCmd returning cmd/stdout/stderr, newXxxTestServer(validToken))
# even when a given test ignores a return or passes the standard token.
- unparam
# In tests, bare type assertions on known fixtures (m["k"].(map[string]any))
# panic loudly as a test failure rather than masking bugs, so check-type-
# assertions noise is excluded here. check-type-assertions still guards
# production code, and real unchecked function errors in tests still fire.
- path: _test\.go
linters:
- errcheck
text: "Error return value is not checked"

issues:
max-issues-per-linter: 0
max-same-issues: 0
2 changes: 2 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func (c *Client) PutFile(ctx context.Context, url string, file *os.File) (*http.
return nil, fmt.Errorf("executing file upload: %w", err)
}
if resp.StatusCode >= 300 || resp.StatusCode < 200 {
defer func() { _ = resp.Body.Close() }()
return nil, ParseErrorResponse(resp)
}
return resp, nil
Expand Down Expand Up @@ -155,6 +156,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer func() { _ = resp.Body.Close() }()
return nil, ParseErrorResponse(resp)
}
return resp, nil
Expand Down
12 changes: 6 additions & 6 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestClient_Get(t *testing.T) {
query := url.Values{"page": []string{"1"}, "limit": []string{"10"}}
resp, err := c.Get(context.Background(), "/api/v1/items", query)
require.NoError(t, err)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

assert.Equal(t, http.MethodGet, gotMethod)
assert.Equal(t, "/api/v1/items", gotPath)
Expand All @@ -98,7 +98,7 @@ func TestClient_GetWithoutQuery(t *testing.T) {
c := NewClient(srv.URL, "tok", "")
resp, err := c.Get(context.Background(), "/test", nil)
require.NoError(t, err)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

assert.Empty(t, gotQuery)
}
Expand All @@ -119,7 +119,7 @@ func TestClient_Post(t *testing.T) {
body := map[string]string{"name": "test", "email": "test@example.com"}
resp, err := c.Post(context.Background(), "/api/v1/items", body)
require.NoError(t, err)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

assert.Equal(t, http.MethodPost, gotMethod)
assert.Equal(t, "application/json", gotContentType)
Expand All @@ -143,7 +143,7 @@ func TestClient_Put(t *testing.T) {
body := map[string]string{"name": "updated"}
resp, err := c.Put(context.Background(), "/api/v1/items/1", body)
require.NoError(t, err)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

assert.Equal(t, http.MethodPut, gotMethod)
assert.Equal(t, "updated", gotBody["name"])
Expand All @@ -161,7 +161,7 @@ func TestClient_Delete(t *testing.T) {
c := NewClient(srv.URL, "tok", "")
resp, err := c.Delete(context.Background(), "/api/v1/items/1")
require.NoError(t, err)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

assert.Equal(t, http.MethodDelete, gotMethod)
assert.Equal(t, "/api/v1/items/1", gotPath)
Expand Down Expand Up @@ -193,7 +193,7 @@ func TestClient_PutFile(t *testing.T) {
// PutFile uses the full URL directly (presigned S3 URL), not BaseURL+path.
resp, err := c.PutFile(context.Background(), srv.URL+"/upload", f)
require.NoError(t, err)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

assert.Equal(t, http.MethodPut, gotMethod)
assert.Equal(t, "vector-cli/test", gotUserAgent)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestAPIError_Error_MultipleValidationErrors(t *testing.T) {

func TestAPIError_ImplementsErrorInterface(t *testing.T) {
var err error = &APIError{Message: "test"}
assert.NotNil(t, err)
require.Error(t, err)
assert.Equal(t, "test", err.Error())
}

Expand Down
4 changes: 2 additions & 2 deletions internal/appctx/appctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func TestNewApp(t *testing.T) {
cfg := &config.Config{ApiURL: "https://example.com"}
cfg := &config.Config{APIURL: "https://example.com"}
client := api.NewClient("https://example.com", "test-key", "")

app := appctx.NewApp(cfg, client, "")
Expand All @@ -24,7 +24,7 @@ func TestNewApp(t *testing.T) {
}

func TestContextRoundTrip(t *testing.T) {
cfg := &config.Config{ApiURL: "https://example.com"}
cfg := &config.Config{APIURL: "https://example.com"}
client := api.NewClient("https://example.com", "test-key", "")
app := appctx.NewApp(cfg, client, "")

Expand Down
10 changes: 5 additions & 5 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ func NewRootCmd() *cobra.Command {
}

// 3. Build API client
client := api.NewClient(cfg.ApiURL, token, "")
client := api.NewClient(cfg.APIURL, token, "")

// 4. Detect output format from --json/--no-json flags
jsonFlag, _ := cmd.Flags().GetBool("json")
noJsonFlag, _ := cmd.Flags().GetBool("no-json")
format := output.DetectFormat(jsonFlag, noJsonFlag)
noJSONFlag, _ := cmd.Flags().GetBool("no-json")
format := output.DetectFormat(jsonFlag, noJSONFlag)

// 5. Handle --jq flag
jqExpr, _ := cmd.Flags().GetString("jq")
var writerOpts []output.WriterOption

if jqExpr != "" {
if noJsonFlag {
if noJSONFlag {
return fmt.Errorf("--jq and --no-json cannot be used together")
}

Expand Down Expand Up @@ -125,7 +125,7 @@ func NewRootCmd() *cobra.Command {
cmd.AddCommand(commands.NewBackupCmd())
cmd.AddCommand(commands.NewRestoreCmd())
cmd.AddCommand(commands.NewWafCmd())
cmd.AddCommand(commands.NewDbCmd())
cmd.AddCommand(commands.NewDBCmd())
cmd.AddCommand(commands.NewArchiveCmd())
cmd.AddCommand(commands.NewMcpCmd())
cmd.AddCommand(commands.NewSkillCmd())
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestPersistentPreRunE_LoadsDefaultConfig(t *testing.T) {
err := cmd.Execute()
require.NoError(t, err)
require.NotNil(t, captured)
assert.Equal(t, "https://api.builtfast.com", captured.Config.ApiURL)
assert.Equal(t, "https://api.builtfast.com", captured.Config.APIURL)
}

func TestPersistentPreRunE_TokenFromFlag(t *testing.T) {
Expand Down Expand Up @@ -342,7 +342,7 @@ func TestPersistentPreRunE_CustomAPIURL(t *testing.T) {
t.Setenv("VECTOR_CONFIG_DIR", tmpDir)

// Write custom config
cfg := config.Config{ApiURL: "https://custom.api.com"}
cfg := config.Config{APIURL: "https://custom.api.com"}
data, err := json.MarshalIndent(cfg, "", " ")
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "config.json"), data, 0o644))
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func newAccountShowCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

var item map[string]any
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/account_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func newAccountAPIKeyListCmd() *cobra.Command {
if err != nil {
return fmt.Errorf("failed to list API keys: %w", err)
}
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

data, meta, err := parseResponseWithMeta(body)
Expand Down Expand Up @@ -148,7 +148,7 @@ func newAccountAPIKeyCreateCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

var item map[string]any
Expand Down Expand Up @@ -210,7 +210,7 @@ func newAccountAPIKeyDeleteCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

output.PrintMessage(cmd.OutOrStdout(), "API key deleted successfully.")
Expand Down
10 changes: 5 additions & 5 deletions internal/commands/account_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func newAccountSecretListCmd() *cobra.Command {
if err != nil {
return fmt.Errorf("failed to list secrets: %w", err)
}
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

data, meta, err := parseResponseWithMeta(body)
Expand Down Expand Up @@ -134,7 +134,7 @@ func newAccountSecretShowCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

var item map[string]any
Expand Down Expand Up @@ -205,7 +205,7 @@ func newAccountSecretCreateCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

var item map[string]any
Expand Down Expand Up @@ -282,7 +282,7 @@ func newAccountSecretUpdateCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

var item map[string]any
Expand Down Expand Up @@ -345,7 +345,7 @@ func newAccountSecretDeleteCmd() *cobra.Command {
}

if app.Output.Format() == output.JSON {
return app.Output.JSON(json.RawMessage(data))
return app.Output.JSON(data)
}

app.Output.Message("Secret deleted successfully.")
Expand Down
Loading
Loading