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.
This commit is contained in:
2026-10-04 17:32:44 +02:00
parent aa0821f169
commit 4d57318e86
6 changed files with 115 additions and 15 deletions

View File

@@ -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

41
backend/.golangci.yml Normal file
View File

@@ -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

View File

@@ -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{
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://")
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}