Skip to content

Commit 5bec0c3

Browse files
authored
ci: enable strict golangci-lint (#782)
1 parent 5c8e727 commit 5bec0c3

177 files changed

Lines changed: 1975 additions & 1525 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: GolangCI-Lint
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
golangci-lint:
11+
name: Strict lint
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Check out repository
16+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
20+
with:
21+
go-version: '1.26.4'
22+
cache: true
23+
24+
- name: Install golangci-lint
25+
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
26+
27+
- name: Run strict GolangCI-Lint
28+
run: scripts/check-golangci-lint.sh

.golangci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
version: "2"
2+
3+
run:
4+
timeout: 10m
5+
tests: true
6+
modules-download-mode: readonly
7+
8+
linters:
9+
default: none
10+
enable:
11+
- asasalint
12+
- asciicheck
13+
- bidichk
14+
- bodyclose
15+
- copyloopvar
16+
- durationcheck
17+
- errcheck
18+
- errchkjson
19+
- errorlint
20+
- fatcontext
21+
- gocheckcompilerdirectives
22+
- gocritic
23+
- gomoddirectives
24+
- govet
25+
- ineffassign
26+
- makezero
27+
- misspell
28+
- nilerr
29+
- nilnesserr
30+
- nilnil
31+
- nolintlint
32+
- nosprintfhostport
33+
- predeclared
34+
- rowserrcheck
35+
- sqlclosecheck
36+
- staticcheck
37+
- testableexamples
38+
- thelper
39+
- unconvert
40+
- unparam
41+
- unused
42+
- wastedassign
43+
44+
issues:
45+
max-issues-per-linter: 0
46+
max-same-issues: 0
47+
uniq-by-line: false
48+
49+
formatters:
50+
enable:
51+
- gci
52+
- gofmt
53+
- gofumpt
54+
- goimports

addons/auth/auth_test.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ func TestSessionRejectsTamperedCookie(t *testing.T) {
691691
}
692692
cookie := recorder.Result().Cookies()[0]
693693
// Flip the role inside the signed payload without re-signing.
694-
cookie.Value = cookie.Value + "x"
694+
cookie.Value += "x"
695695

696696
request := httptest.NewRequest(http.MethodGet, "/", nil)
697697
request.AddCookie(cookie)
@@ -825,9 +825,9 @@ func TestSessionCookieDefaultsToSecureAttributes(t *testing.T) {
825825
if !cookie.Expires.Equal(now.Add(DefaultSessionTTL)) {
826826
t.Fatalf("cookie expires at %v, want %v", cookie.Expires, now.Add(DefaultSessionTTL))
827827
}
828-
clear := sessions.ClearCookie()
829-
if clear.Name != DefaultSessionCookie || clear.Path != "/" || !clear.HttpOnly || !clear.Secure || clear.SameSite != http.SameSiteLaxMode {
830-
t.Fatalf("unexpected default clear-cookie attributes: %#v", clear)
828+
clearCookie := sessions.ClearCookie()
829+
if clearCookie.Name != DefaultSessionCookie || clearCookie.Path != "/" || !clearCookie.HttpOnly || !clearCookie.Secure || clearCookie.SameSite != http.SameSiteLaxMode {
830+
t.Fatalf("unexpected default clear-cookie attributes: %#v", clearCookie)
831831
}
832832
}
833833

@@ -851,9 +851,9 @@ func TestSessionCookieHelpers(t *testing.T) {
851851
if cookie.Name != DefaultSessionCookie || cookie.Value == "" || !cookie.HttpOnly {
852852
t.Fatalf("unexpected issued cookie: %#v", cookie)
853853
}
854-
clear := sessions.ClearCookie()
855-
if clear.Name != DefaultSessionCookie || clear.MaxAge >= 0 {
856-
t.Fatalf("unexpected clear cookie: %#v", clear)
854+
clearCookie := sessions.ClearCookie()
855+
if clearCookie.Name != DefaultSessionCookie || clearCookie.MaxAge >= 0 {
856+
t.Fatalf("unexpected clear cookie: %#v", clearCookie)
857857
}
858858
}
859859

@@ -1085,5 +1085,7 @@ func TestNewReportsEnvNameWithoutLeakingSecret(t *testing.T) {
10851085

10861086
// Sessions must satisfy the Provider interface so it can be registered with the
10871087
// generated RegisterAuthProvider hook.
1088-
var _ Provider = (*Sessions)(nil)
1089-
var _ PasswordHasher = PBKDF2Hasher{}
1088+
var (
1089+
_ Provider = (*Sessions)(nil)
1090+
_ PasswordHasher = PBKDF2Hasher{}
1091+
)

addons/auth/password.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"encoding/base64"
99
"errors"
1010
"fmt"
11-
"hash"
1211
"strconv"
1312
"strings"
1413
)
@@ -144,5 +143,5 @@ func decodeHash(encoded string) (iterations int, salt, key []byte, err error) {
144143
}
145144

146145
func pbkdf2SHA256(password string, salt []byte, iterations, keyLength int) ([]byte, error) {
147-
return pbkdf2.Key(func() hash.Hash { return sha256.New() }, password, salt, iterations, keyLength)
146+
return pbkdf2.Key(sha256.New, password, salt, iterations, keyLength)
148147
}

addons/auth/session.go

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func sessionSigningKeys(options Options) (SigningKey, map[string]SigningKey, err
263263

264264
func validateSigningKeyID(label string, id string) error {
265265
if strings.Contains(id, ".") {
266-
return fmt.Errorf("gowdk auth: %s id %q must not contain .", label, id)
266+
return fmt.Errorf("gowdk auth: %s id %q must not contain dot", label, id)
267267
}
268268
return nil
269269
}
@@ -406,12 +406,12 @@ func (sessions *Sessions) Revoke(ctx context.Context, request *http.Request) err
406406
if sessions.mode != SessionModeRevocable || request == nil {
407407
return nil
408408
}
409-
cookie, err := request.Cookie(sessions.cookie)
410-
if err != nil {
409+
cookie, ok := sessions.requestCookie(request)
410+
if !ok {
411411
return nil
412412
}
413-
payload, err := sessions.verify(cookie.Value)
414-
if err != nil {
413+
payload, ok := sessions.verifiedPayload(cookie.Value)
414+
if !ok {
415415
return nil
416416
}
417417
if strings.TrimSpace(payload.SessionID) == "" {
@@ -456,24 +456,24 @@ func (sessions *Sessions) ClearCookie() http.Cookie {
456456
// principal and no error, meaning unauthenticated.
457457
func (sessions *Sessions) Principal(request *http.Request) (*Principal, error) {
458458
if request == nil {
459-
return nil, nil
459+
return unauthenticatedPrincipal()
460460
}
461-
cookie, err := request.Cookie(sessions.cookie)
462-
if err != nil {
463-
return nil, nil
461+
cookie, ok := sessions.requestCookie(request)
462+
if !ok {
463+
return unauthenticatedPrincipal()
464464
}
465-
payload, err := sessions.verify(cookie.Value)
466-
if err != nil {
467-
return nil, nil
465+
payload, ok := sessions.verifiedPayload(cookie.Value)
466+
if !ok {
467+
return unauthenticatedPrincipal()
468468
}
469469
if sessions.now().Unix() >= payload.Expires {
470-
return nil, nil
470+
return unauthenticatedPrincipal()
471471
}
472472
if sessions.mode == SessionModeRevocable {
473473
return sessions.revocablePrincipal(request.Context(), payload)
474474
}
475475
if strings.TrimSpace(payload.ID) == "" {
476-
return nil, nil
476+
return unauthenticatedPrincipal()
477477
}
478478
return &Principal{
479479
ID: payload.ID,
@@ -485,21 +485,21 @@ func (sessions *Sessions) Principal(request *http.Request) (*Principal, error) {
485485

486486
func (sessions *Sessions) revocablePrincipal(ctx context.Context, payload sessionPayload) (*Principal, error) {
487487
if strings.TrimSpace(payload.SessionID) == "" {
488-
return nil, nil
488+
return unauthenticatedPrincipal()
489489
}
490490
record, err := sessions.store.LookupSession(ctx, payload.SessionID)
491491
if err != nil {
492492
if errors.Is(err, ErrSessionNotFound) {
493-
return nil, nil
493+
return unauthenticatedPrincipal()
494494
}
495495
return nil, err
496496
}
497497
now := sessions.now()
498498
if record.Revoked || record.expired(now) || strings.TrimSpace(record.Principal.ID) == "" {
499-
return nil, nil
499+
return unauthenticatedPrincipal()
500500
}
501501
if sessionRecordAuthorizationVersion(record) != payload.AuthorizationVersion {
502-
return nil, nil
502+
return unauthenticatedPrincipal()
503503
}
504504
if sessions.idleTTL > 0 {
505505
toucher, ok := sessions.store.(SessionToucher)
@@ -516,6 +516,27 @@ func (sessions *Sessions) revocablePrincipal(ctx context.Context, payload sessio
516516
return &principal, nil
517517
}
518518

519+
func unauthenticatedPrincipal() (*Principal, error) {
520+
var principal *Principal
521+
return principal, nil
522+
}
523+
524+
func (sessions *Sessions) requestCookie(request *http.Request) (*http.Cookie, bool) {
525+
cookie, err := request.Cookie(sessions.cookie)
526+
if err != nil {
527+
return nil, false
528+
}
529+
return cookie, true
530+
}
531+
532+
func (sessions *Sessions) verifiedPayload(token string) (sessionPayload, bool) {
533+
payload, err := sessions.verify(token)
534+
if err != nil {
535+
return sessionPayload{}, false
536+
}
537+
return payload, true
538+
}
539+
519540
func sessionRecordAuthorizationVersion(record SessionRecord) string {
520541
if record.Principal.AuthorizationVersion != "" {
521542
return record.Principal.AuthorizationVersion

addons/db/db.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ func OpenWithOptions(driver, dsn string, options Options) (*sql.DB, error) {
8181
ctx, cancel := context.WithTimeout(context.Background(), pingTimeout)
8282
defer cancel()
8383
if err := database.PingContext(ctx); err != nil {
84-
database.Close()
84+
if closeErr := database.Close(); closeErr != nil {
85+
return nil, fmt.Errorf("gowdk db: ping %q: %w; close: %w", driver, err, closeErr)
86+
}
8587
return nil, fmt.Errorf("gowdk db: ping %q: %w", driver, err)
8688
}
8789
return database, nil

addons/db/db_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func TestApplyMigrationsReservesBeforeRunningMigrationSQL(t *testing.T) {
186186
if reserve < 0 || migration < 0 || finalize < 0 {
187187
t.Fatalf("did not find reservation, migration, and finalize statements in %#v", state.executed)
188188
}
189-
if !(reserve < migration && migration < finalize) {
189+
if reserve >= migration || migration >= finalize {
190190
t.Fatalf("migration was not reserved before user SQL and finalized after it: %#v", state.executed)
191191
}
192192
}

addons/realtime/realtime_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,5 @@ func TestNewSSEReturnsPresentationFanout(t *testing.T) {
2222
WithSSEReplayLimit(4),
2323
WithSSEAudienceFromRequest(func(*http.Request) []string { return []string{"tenant:test"} }),
2424
)
25-
if fanout == nil {
26-
t.Fatal("expected SSE fanout")
27-
}
25+
_ = fanout
2826
}

addons/tailwind/tailwind.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ func (p processor) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error)
7272
if err != nil {
7373
return gowdk.CSSResult{}, err
7474
}
75-
defer os.RemoveAll(tempDir)
75+
defer func() {
76+
_ = os.RemoveAll(tempDir)
77+
}()
7678

7779
tempOutput := filepath.Join(tempDir, "app.css")
7880
workingDir := cssWorkingDir(context)

docs/engineering/ci.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Required pull-request lanes:
3737

3838
The release lanes live outside the pull-request CI workflow:
3939

40+
- `.github/workflows/golangci-lint.yml`: manual strict lint workflow. It runs
41+
`scripts/check-golangci-lint.sh` with the pinned GolangCI-Lint version. Do
42+
not make this a required pull-request lane until the repository is clean under
43+
the configured lint set.
4044
- `.github/workflows/release-dry-run.yml`: scheduled weekly and manual; packages
4145
CLI/VS Code artifacts, writes checksums, and uploads workflow artifacts. This
4246
is GitHub-only because it uses Actions artifact upload.
@@ -58,6 +62,7 @@ Run the same local checks before handoff when relevant:
5862
scripts/vulncheck-go-modules.sh
5963
go build ./cmd/gowdk
6064
scripts/check-dead-code.sh
65+
scripts/check-golangci-lint.sh
6166
```
6267

6368
- VS Code extension checks:
@@ -177,6 +182,27 @@ after confirming a clean baseline without broad suppressions; if an intentional
177182
entry point needs a suppression, keep it local to the declaration and document
178183
why external reachability is expected.
179184

185+
## GolangCI-Lint
186+
187+
`.golangci.yml` is intentionally strict:
188+
189+
```sh
190+
scripts/check-golangci-lint.sh
191+
```
192+
193+
The gate verifies the config and runs `golangci-lint run` with correctness,
194+
resource-handling, formatting, maintainability, and dead-code linters enabled.
195+
Test files are included, module downloads are readonly, issue counts are
196+
uncapped, and existing findings are not hidden through exclusions.
197+
The wrapper uses `golangci-lint` when `v2.12.2` is on `PATH`; otherwise it falls back to
198+
`go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2`. CI
199+
also installs that pinned version before running the wrapper.
200+
201+
Do not add broad `linters.exclusions` or `issues.exclude` entries to make a
202+
dirty baseline pass. Fix the finding, add a narrow `//nolint:<linter>` with a
203+
reason where the code is intentionally exceptional, or lower strictness only
204+
with an owning engineering note that explains the tradeoff.
205+
180206
## Release Smoke
181207

182208
After publishing a tag, verify the current machine's release artifact locally:

0 commit comments

Comments
 (0)