From 4d57318e8603266fbc7f074800ccd98cc8031ce0 Mon Sep 17 00:00:00 2001 From: LovroG05 Date: Sun, 4 Oct 2026 17:32:44 +0200 Subject: [PATCH] ci: run tests, lint and the UI build on every push Add a golangci-lint config and fix its findings: the OIDC state cookie gets the Secure flag on https deployments, and pool sizes are clamped before the int32 conversion. --- .woodpecker.yml | 46 +++++++++++++++++-- backend/.golangci.yml | 41 +++++++++++++++++ backend/internal/api/oidclogin.go | 23 +++++++--- backend/internal/config/config.go | 16 ++++++- backend/internal/providers/copilot/copilot.go | 2 +- .../internal/providers/openrouter/images.go | 2 +- 6 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 backend/.golangci.yml diff --git a/.woodpecker.yml b/.woodpecker.yml index 04dc411..4509da9 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,14 +1,50 @@ -# Builds the LMR image and pushes it to the registry on push to master. +# CI for LMR. Every push and pull request runs the checks. A push to master +# then builds the image and pushes it to the author's registry; replace the +# registry settings and secrets with yours when you fork. when: - - event: push - branch: master - - event: manual - branch: master + - event: [push, pull_request, manual] + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: gateway + POSTGRES_PASSWORD: gateway + POSTGRES_DB: gateway steps: + test: + image: golang:1.27-alpine + environment: + CGO_ENABLED: "0" + GATEWAY_TEST_DB_URL: postgres://gateway:gateway@postgres:5432/gateway?sslmode=disable + commands: + - cd backend + - go vet ./... + - i=0; until nc -z postgres 5432 || [ $i -ge 60 ]; do i=$((i+1)); sleep 1; done + - GATEWAY_DB_URL=$GATEWAY_TEST_DB_URL go run ./cmd/migrate up + - go test -count=1 ./... + + lint: + image: golangci/golangci-lint:v2.14.0 + commands: + - cd backend + - golangci-lint run ./... + + ui: + image: node:22-alpine + commands: + - cd backend/dashboard + - npm ci + - npx tsc --noEmit + - npm run build + build-and-push: image: woodpeckerci/plugin-docker-buildx:6 privileged: true + when: + - event: [push, manual] + branch: master settings: registry: registry.govekar.net repo: registry.govekar.net/lovrog05/localmodelroutex diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 0000000..b9baf96 --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,41 @@ +# golangci-lint v2 configuration. Run with `make lint`. +version: "2" + +run: + timeout: 5m + tests: true + +linters: + default: standard # errcheck, govet, ineffassign, staticcheck, unused + enable: + - misspell + - unconvert + - unparam + - bodyclose + - noctx + - gosec + settings: + gosec: + excludes: + - G104 # unhandled errors are covered by errcheck + - G404 # math/rand is fine for round-robin routing + exclusions: + generated: lax + presets: + - comments + - std-error-handling + rules: + - path: _test\.go + linters: + - gosec + - noctx + - bodyclose + +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - git.govekar.net/LovroG05/localmodelroutex diff --git a/backend/internal/api/oidclogin.go b/backend/internal/api/oidclogin.go index 663fc34..e595cb1 100644 --- a/backend/internal/api/oidclogin.go +++ b/backend/internal/api/oidclogin.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "net/http" "net/url" + "strings" "time" "golang.org/x/oauth2" @@ -27,12 +28,13 @@ func (a *API) oidcLogin(w http.ResponseWriter, r *http.Request) { _, _ = rand.Read(b) state := base64.RawURLEncoding.EncodeToString(b) - http.SetCookie(w, &http.Cookie{ + http.SetCookie(w, &http.Cookie{ //nolint:gosec // G124: Secure follows the public URL scheme, see secureCookies Name: oidcStateCookie, Value: state, Path: "/auth", MaxAge: int(oidcStateTTL.Seconds()), HttpOnly: true, + Secure: a.secureCookies(), SameSite: http.SameSiteLaxMode, }) @@ -55,11 +57,14 @@ func (a *API) oidcCallback(w http.ResponseWriter, r *http.Request) { return } // Consume the state cookie. - http.SetCookie(w, &http.Cookie{ - Name: oidcStateCookie, - Value: "", - Path: "/auth", - MaxAge: -1, + http.SetCookie(w, &http.Cookie{ //nolint:gosec // G124: Secure follows the public URL scheme, see secureCookies + Name: oidcStateCookie, + Value: "", + Path: "/auth", + MaxAge: -1, + HttpOnly: true, + Secure: a.secureCookies(), + SameSite: http.SameSiteLaxMode, }) tok, err := a.oauth2Config().Exchange(r.Context(), r.URL.Query().Get("code")) @@ -105,3 +110,9 @@ func (a *API) oauth2Config() *oauth2.Config { Endpoint: oauth2.Endpoint{AuthURL: a.OIDCAuthURL, TokenURL: a.OIDCTokenURL}, } } + +// secureCookies reports whether cookies get the Secure flag. The public URL +// decides, so a plain http://localhost dev setup keeps working. +func (a *API) secureCookies() bool { + return strings.HasPrefix(strings.ToLower(a.PublicBaseURL), "https://") +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index bfc0a11..d1cea9d 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "math" "strings" "time" @@ -70,8 +71,8 @@ func Load() (*Config, error) { }, DB: DBConfig{ URL: v.GetString("db.url"), - MaxConns: int32(v.GetInt("db.max_conns")), - MinConns: int32(v.GetInt("db.min_conns")), + MaxConns: clampInt32(v.GetInt("db.max_conns")), + MinConns: clampInt32(v.GetInt("db.min_conns")), MaxConnLifetime: v.GetInt("db.max_conn_lifetime"), }, OIDC: OIDCConfig{ @@ -128,3 +129,14 @@ func (c *Config) validate() error { func (c *Config) IsDev() bool { return c.Server.Env == "development" } + +// clampInt32 converts a pool size setting to int32 without overflow. +func clampInt32(n int) int32 { + if n < 0 { + return 0 + } + if n > math.MaxInt32 { + return math.MaxInt32 + } + return int32(n) +} diff --git a/backend/internal/providers/copilot/copilot.go b/backend/internal/providers/copilot/copilot.go index 175cc29..c1056cf 100644 --- a/backend/internal/providers/copilot/copilot.go +++ b/backend/internal/providers/copilot/copilot.go @@ -18,7 +18,7 @@ import ( ) const ( - tokenURL = "https://api.github.com/copilot_internal/v2/token" + tokenURL = "https://api.github.com/copilot_internal/v2/token" //nolint:gosec // G101: a URL, not a credential chatBaseURL = "https://api.githubcopilot.com" // refreshSkew refreshes the token slightly before it actually expires. refreshSkew = time.Minute diff --git a/backend/internal/providers/openrouter/images.go b/backend/internal/providers/openrouter/images.go index e61d15a..7b2e68b 100644 --- a/backend/internal/providers/openrouter/images.go +++ b/backend/internal/providers/openrouter/images.go @@ -53,7 +53,7 @@ func (p *provider) ImageGeneration(ctx context.Context, req *providers.ImageRequ cost float64 ) for i := 0; i < n; i++ { - resp, err := p.Transport.ChatCompletion(ctx, chatReq) + resp, err := p.ChatCompletion(ctx, chatReq) if err != nil { return nil, err }