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:
@@ -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:
|
when:
|
||||||
- event: push
|
- event: [push, pull_request, manual]
|
||||||
branch: master
|
|
||||||
- event: manual
|
services:
|
||||||
branch: master
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: gateway
|
||||||
|
POSTGRES_PASSWORD: gateway
|
||||||
|
POSTGRES_DB: gateway
|
||||||
|
|
||||||
steps:
|
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:
|
build-and-push:
|
||||||
image: woodpeckerci/plugin-docker-buildx:6
|
image: woodpeckerci/plugin-docker-buildx:6
|
||||||
privileged: true
|
privileged: true
|
||||||
|
when:
|
||||||
|
- event: [push, manual]
|
||||||
|
branch: master
|
||||||
settings:
|
settings:
|
||||||
registry: registry.govekar.net
|
registry: registry.govekar.net
|
||||||
repo: registry.govekar.net/lovrog05/localmodelroutex
|
repo: registry.govekar.net/lovrog05/localmodelroutex
|
||||||
|
|||||||
41
backend/.golangci.yml
Normal file
41
backend/.golangci.yml
Normal 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
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -27,12 +28,13 @@ func (a *API) oidcLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = rand.Read(b)
|
_, _ = rand.Read(b)
|
||||||
state := base64.RawURLEncoding.EncodeToString(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,
|
Name: oidcStateCookie,
|
||||||
Value: state,
|
Value: state,
|
||||||
Path: "/auth",
|
Path: "/auth",
|
||||||
MaxAge: int(oidcStateTTL.Seconds()),
|
MaxAge: int(oidcStateTTL.Seconds()),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: a.secureCookies(),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -55,11 +57,14 @@ func (a *API) oidcCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Consume the state cookie.
|
// 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,
|
Name: oidcStateCookie,
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/auth",
|
Path: "/auth",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: a.secureCookies(),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
tok, err := a.oauth2Config().Exchange(r.Context(), r.URL.Query().Get("code"))
|
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},
|
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://")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -70,8 +71,8 @@ func Load() (*Config, error) {
|
|||||||
},
|
},
|
||||||
DB: DBConfig{
|
DB: DBConfig{
|
||||||
URL: v.GetString("db.url"),
|
URL: v.GetString("db.url"),
|
||||||
MaxConns: int32(v.GetInt("db.max_conns")),
|
MaxConns: clampInt32(v.GetInt("db.max_conns")),
|
||||||
MinConns: int32(v.GetInt("db.min_conns")),
|
MinConns: clampInt32(v.GetInt("db.min_conns")),
|
||||||
MaxConnLifetime: v.GetInt("db.max_conn_lifetime"),
|
MaxConnLifetime: v.GetInt("db.max_conn_lifetime"),
|
||||||
},
|
},
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
@@ -128,3 +129,14 @@ func (c *Config) validate() error {
|
|||||||
func (c *Config) IsDev() bool {
|
func (c *Config) IsDev() bool {
|
||||||
return c.Server.Env == "development"
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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"
|
chatBaseURL = "https://api.githubcopilot.com"
|
||||||
// refreshSkew refreshes the token slightly before it actually expires.
|
// refreshSkew refreshes the token slightly before it actually expires.
|
||||||
refreshSkew = time.Minute
|
refreshSkew = time.Minute
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (p *provider) ImageGeneration(ctx context.Context, req *providers.ImageRequ
|
|||||||
cost float64
|
cost float64
|
||||||
)
|
)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
resp, err := p.Transport.ChatCompletion(ctx, chatReq)
|
resp, err := p.ChatCompletion(ctx, chatReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user