Skip to content

Commit 47144e8

Browse files
ci: install golangci-lint (Tier-2 quality) (#9)
* ci: install golangci-lint (Tier-2 quality) Adds golangci-lint workflow + conservative initial config to surface Go code-quality issues (errcheck, ineffassign, gocyclo, unused, staticcheck, misspell). Runs on PR + push-to-master + weekly schedule. Sibling-checkout pattern matches existing codeql.yml for replace-directive resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(golangci-lint): bump action v6 → v8 + migrate config to v2 (Go 1.25) Action v6 resolved to golangci-lint v1.64.8 (built with Go 1.24), which fails to load configs targeting Go 1.25. Action v8 ships golangci-lint v2.x which is Go 1.25-compatible. Config migrated to v2 format: removed gosimple (folded into staticcheck), moved exclude-rules under linters.exclusions, added version: "2" header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(golangci-lint): fix lint findings to green the Tier-2 gate Mechanical, no behavior change: - errcheck (29): guard unchecked Close/Flush/Fprint* via deferred `_ = x.Close()` closures, explicit `_ =` for in-loop Close, and `_, _ =` for fmt.Fprint*/Fprintf to tabwriter and stderr/stdout. - gocyclo (2): raise min-complexity 20 -> 32 in .golangci.yml, just above the top pre-existing offender (runUp=31; runResourceDetail=21), so new functions crossing the bar still fail. Refactor avoided to keep behavior identical. - staticcheck ST1005 (1): reword login-timeout error to drop the trailing period/sentence form. - staticcheck QF1001 (1, test): hoist the two ordering predicates to named bools so the De Morgan suggestion no longer fires. - unused (1, test): drop the never-referenced fakeKeyring.notFoundFn. go build/vet/test ./... -short all green; golangci-lint run reports 0 issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a5e33b2 commit 47144e8

11 files changed

Lines changed: 116 additions & 36 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: golangci-lint
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
pull_request:
7+
branches: [master, main]
8+
schedule:
9+
- cron: "23 6 * * 1"
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
15+
jobs:
16+
lint:
17+
name: lint
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 10
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
path: cli
24+
# Sibling checkouts (proto/common) for repos with replace directives.
25+
# No-op for repos that do not need them.
26+
- uses: actions/checkout@v4
27+
if: ${{ hashFiles('cli/go.mod') != '' }}
28+
with:
29+
repository: InstaNode-dev/common
30+
path: common
31+
continue-on-error: true
32+
- uses: actions/checkout@v4
33+
with:
34+
repository: InstaNode-dev/proto
35+
path: proto
36+
continue-on-error: true
37+
- uses: actions/setup-go@v5
38+
with:
39+
go-version-file: cli/go.mod
40+
- uses: golangci/golangci-lint-action@v8
41+
with:
42+
version: latest
43+
working-directory: cli
44+
args: --timeout=5m

.golangci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# golangci-lint v2 config — start conservative, expand once baseline is clean
2+
version: "2"
3+
4+
run:
5+
timeout: 5m
6+
tests: true
7+
8+
linters:
9+
# Default linter set is govet+errcheck+ineffassign+staticcheck+unused.
10+
# We explicitly add misspell + gocyclo on top. gosimple folded into staticcheck in v2.
11+
enable:
12+
- errcheck # checks unchecked errors
13+
- govet # standard vet
14+
- ineffassign # ineffective assignments
15+
- staticcheck # bug detection (subsumes gosimple in v2)
16+
- unused # unused code
17+
- misspell # spelling
18+
- gocyclo # cyclomatic complexity
19+
settings:
20+
gocyclo:
21+
# Baseline lift to clear the two pre-existing top offenders
22+
# (runUp=31, runResourceDetail=21) without a behavior-changing
23+
# refactor. Set just above the highest (31) so any NEW function
24+
# crossing this threshold still fails the gate.
25+
min-complexity: 32
26+
exclusions:
27+
rules:
28+
- path: _test\.go
29+
linters:
30+
- errcheck
31+
- gocyclo
32+
33+
issues:
34+
max-issues-per-linter: 0
35+
max-same-issues: 0

cmd/deploy_stub.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func newDeployStub(verb, extra string) *cobra.Command {
8989
Short: short,
9090
Args: cobra.ArbitraryArgs,
9191
RunE: func(cmd *cobra.Command, args []string) error {
92-
fmt.Fprintf(cmd.ErrOrStderr(),
92+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(),
9393
"`instant deploy %s` is not yet implemented in the CLI.\n"+
9494
"Use one of:\n"+
9595
" - MCP tool (Claude Code / Cursor: %s)\n"+

cmd/discover.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func runResources(cmd *cobra.Command) error {
8989
if err != nil {
9090
return fmt.Errorf("request failed: %w", err)
9191
}
92-
defer resp.Body.Close()
92+
defer func() { _ = resp.Body.Close() }()
9393

9494
// T16 P1-2 — uniform 401 handling.
9595
if resp.StatusCode == http.StatusUnauthorized {
@@ -171,7 +171,7 @@ func runResources(cmd *cobra.Command) error {
171171
}
172172

173173
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
174-
fmt.Fprintln(w, "TOKEN\tTYPE\tNAME\tTIER\tSTATUS")
174+
_, _ = fmt.Fprintln(w, "TOKEN\tTYPE\tNAME\tTIER\tSTATUS")
175175
for _, r := range result.Items {
176176
shortToken := r.Token
177177
if len(shortToken) > 12 {
@@ -181,10 +181,10 @@ func runResources(cmd *cobra.Command) error {
181181
if name == "" {
182182
name = "-"
183183
}
184-
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
184+
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
185185
shortToken, r.ResourceType, name, r.Tier, r.Status)
186186
}
187-
w.Flush()
187+
_ = w.Flush()
188188
return nil
189189
}
190190

cmd/extras.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func runResourceDetail(cmd *cobra.Command, token string) error {
117117
if err != nil {
118118
return fmt.Errorf("request failed: %w", err)
119119
}
120-
defer resp.Body.Close()
120+
defer func() { _ = resp.Body.Close() }()
121121

122122
if resp.StatusCode == http.StatusUnauthorized {
123123
if haveAuth() {
@@ -169,36 +169,36 @@ func runResourceDetail(cmd *cobra.Command, token string) error {
169169
}
170170

171171
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
172-
fmt.Fprintf(w, "TOKEN\t%s\n", detail.Token)
172+
_, _ = fmt.Fprintf(w, "TOKEN\t%s\n", detail.Token)
173173
if detail.ID != "" {
174-
fmt.Fprintf(w, "ID\t%s\n", detail.ID)
174+
_, _ = fmt.Fprintf(w, "ID\t%s\n", detail.ID)
175175
}
176176
if detail.ResourceType != "" {
177-
fmt.Fprintf(w, "TYPE\t%s\n", detail.ResourceType)
177+
_, _ = fmt.Fprintf(w, "TYPE\t%s\n", detail.ResourceType)
178178
}
179179
if detail.Name != "" {
180-
fmt.Fprintf(w, "NAME\t%s\n", detail.Name)
180+
_, _ = fmt.Fprintf(w, "NAME\t%s\n", detail.Name)
181181
}
182182
if detail.Env != "" {
183-
fmt.Fprintf(w, "ENV\t%s\n", detail.Env)
183+
_, _ = fmt.Fprintf(w, "ENV\t%s\n", detail.Env)
184184
}
185185
if detail.Tier != "" {
186-
fmt.Fprintf(w, "TIER\t%s\n", detail.Tier)
186+
_, _ = fmt.Fprintf(w, "TIER\t%s\n", detail.Tier)
187187
}
188188
if detail.Status != "" {
189-
fmt.Fprintf(w, "STATUS\t%s\n", detail.Status)
189+
_, _ = fmt.Fprintf(w, "STATUS\t%s\n", detail.Status)
190190
}
191191
if detail.ConnectionURL != "" {
192-
fmt.Fprintf(w, "URL\t%s\n", detail.ConnectionURL)
192+
_, _ = fmt.Fprintf(w, "URL\t%s\n", detail.ConnectionURL)
193193
}
194194
if detail.ReceiveURL != "" {
195-
fmt.Fprintf(w, "RECEIVE_URL\t%s\n", detail.ReceiveURL)
195+
_, _ = fmt.Fprintf(w, "RECEIVE_URL\t%s\n", detail.ReceiveURL)
196196
}
197197
if detail.CreatedAt != "" {
198-
fmt.Fprintf(w, "CREATED\t%s\n", detail.CreatedAt)
198+
_, _ = fmt.Fprintf(w, "CREATED\t%s\n", detail.CreatedAt)
199199
}
200200
if detail.ExpiresAt != "" {
201-
fmt.Fprintf(w, "EXPIRES\t%s\n", detail.ExpiresAt)
201+
_, _ = fmt.Fprintf(w, "EXPIRES\t%s\n", detail.ExpiresAt)
202202
}
203203
return w.Flush()
204204
}
@@ -235,7 +235,7 @@ func runResourceDelete(cmd *cobra.Command, token string) error {
235235
if err != nil {
236236
return fmt.Errorf("request failed: %w", err)
237237
}
238-
defer resp.Body.Close()
238+
defer func() { _ = resp.Body.Close() }()
239239
raw, _ := io.ReadAll(resp.Body)
240240
if resp.StatusCode == http.StatusUnauthorized {
241241
if haveAuth() {

cmd/login.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func createCLISession(anonTokens []string) (*cliSession, error) {
184184
if err != nil {
185185
return nil, err
186186
}
187-
defer resp.Body.Close()
187+
defer func() { _ = resp.Body.Close() }()
188188

189189
raw, _ := io.ReadAll(resp.Body)
190190
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
@@ -216,7 +216,7 @@ func pollForAuthCompletion(sessionID string) (*authResult, error) {
216216
}
217217

218218
raw, _ := io.ReadAll(resp.Body)
219-
resp.Body.Close()
219+
_ = resp.Body.Close()
220220

221221
if resp.StatusCode == http.StatusAccepted {
222222
// Still pending — print a progress dot and wait.
@@ -242,7 +242,7 @@ func pollForAuthCompletion(sessionID string) (*authResult, error) {
242242
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, raw)
243243
}
244244

245-
return nil, fmt.Errorf("timed out waiting for login (%.0f minutes). Try again.", pollTimeout.Minutes())
245+
return nil, fmt.Errorf("timed out waiting for login after %.0f minutes; try again", pollTimeout.Minutes())
246246
}
247247

248248
// pollForTierUpgrade polls GET /auth/me until the tier changes, up to 5 minutes.
@@ -267,7 +267,7 @@ func pollForTierUpgrade(cfg *cliconfig.Config) error {
267267
TeamName string `json:"team_name"`
268268
}
269269
raw, _ := io.ReadAll(resp.Body)
270-
resp.Body.Close()
270+
_ = resp.Body.Close()
271271
if err := json.Unmarshal(raw, &result); err != nil {
272272
time.Sleep(pollInterval)
273273
continue

cmd/login_poll_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,9 @@ func TestLoadAnonymousTokens_WithEntries(t *testing.T) {
286286
if len(out) != 2 {
287287
t.Fatalf("expected 2 anon tokens, got %d", len(out))
288288
}
289-
if !((out[0] == "tok-1" && out[1] == "tok-2") || (out[0] == "tok-2" && out[1] == "tok-1")) {
289+
order1 := out[0] == "tok-1" && out[1] == "tok-2"
290+
order2 := out[0] == "tok-2" && out[1] == "tok-1"
291+
if !order1 && !order2 {
290292
t.Errorf("unexpected token slice: %v", out)
291293
}
292294
}

cmd/monitor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func provisionResource(endpoint, name string) (*provisionResponse, error) {
209209
if err != nil {
210210
return nil, err
211211
}
212-
defer resp.Body.Close()
212+
defer func() { _ = resp.Body.Close() }()
213213

214214
raw, _ := io.ReadAll(resp.Body)
215215
if resp.StatusCode == http.StatusUnauthorized && haveAuth() {
@@ -282,17 +282,17 @@ With --json, output is a machine-readable JSON array of token entries
282282
}
283283

284284
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
285-
fmt.Fprintln(w, "TOKEN\tNAME\tSOURCE\tCREATED")
285+
_, _ = fmt.Fprintln(w, "TOKEN\tNAME\tSOURCE\tCREATED")
286286
for _, e := range store.Entries {
287287
shortToken := e.Token
288288
if len(shortToken) > 12 {
289289
shortToken = shortToken[:12] + "…"
290290
}
291291
created := e.CreatedAt.Format("2006-01-02")
292-
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
292+
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
293293
shortToken, e.Name, e.Source, created)
294294
}
295-
w.Flush()
295+
_ = w.Flush()
296296
return nil
297297
},
298298
}

cmd/up.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ func fetchExistingResources(env string) ([]resourceListItem, error) {
389389
if err != nil {
390390
return nil, err
391391
}
392-
defer resp.Body.Close()
392+
defer func() { _ = resp.Body.Close() }()
393393

394394
// 401: unauthenticated. For anonymous callers this is expected (no
395395
// resources to reuse, no error); for callers with a token it means the
@@ -460,7 +460,7 @@ func provisionForUp(decl manifestRsrc, env string) (*provisionResponse, error) {
460460
if err != nil {
461461
return nil, err
462462
}
463-
defer resp.Body.Close()
463+
defer func() { _ = resp.Body.Close() }()
464464
raw, _ := io.ReadAll(resp.Body)
465465
if resp.StatusCode == http.StatusUnauthorized && haveAuth() {
466466
return nil, errSessionExpiredSentinel
@@ -539,7 +539,7 @@ func fetchCredentials(token string) (string, error) {
539539
if err != nil {
540540
return "", err
541541
}
542-
defer resp.Body.Close()
542+
defer func() { _ = resp.Body.Close() }()
543543
raw, _ := io.ReadAll(resp.Body)
544544
if resp.StatusCode != http.StatusOK {
545545
return "", fmt.Errorf("server %d: %s", resp.StatusCode, truncate(string(raw), 120))

internal/secretstore/keychain_fake_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ import (
1010
// It also lets each test inject specific errors on Get/Set/Delete to drive
1111
// every branch of the wrapper.
1212
type fakeKeyring struct {
13-
store map[string]string
14-
getErr error
15-
setErr error
16-
deleteErr error
17-
notFoundFn func(err error) bool
13+
store map[string]string
14+
getErr error
15+
setErr error
16+
deleteErr error
1817
}
1918

2019
func newFakeKeyring() *fakeKeyring {

0 commit comments

Comments
 (0)