.PHONY: all build run demo clean tidy mod-check frontend frontend-check frontend-stub test test-race test-dashboard test-e2e test-integration test-contract test-live test-all lint lint-fix fix fix-check record-api swagger docs-openapi helm-lint install-tools perf-check perf-bench fuzz infra image seed-demo-data build-plugins image-plugins example-plugins all: frontend build # Get version info VERSION ?= $(shell git describe --tags --always --dirty) COMMIT ?= $(shell git rev-parse --short HEAD) DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") DOCS_API_SERVERS ?= http://localhost:8080 LOG_LEVEL ?= debug SWAGGER_ENABLED ?= true # Build tags covering every file the linter and fixers must see. Without these, # tag-gated files (tests/e2e, tests/integration, tests/contract, live provider # tests) are skipped. BUILD_TAGS ?= swagger,e2e,integration,contract,live GOLANGCI_LINT_VERSION := 2.14.0 GOLANGCI_LINT ?= $(shell go env GOPATH)/bin/golangci-lint # Linker flags to inject version info LDFLAGS := -X "github.com/enterpilot/gomodel/internal/version.Version=$(VERSION)" \ -X "github.com/enterpilot/gomodel/internal/version.Commit=$(COMMIT)" \ -X "github.com/enterpilot/gomodel/internal/version.Date=$(DATE)" install-tools: @installed_version="$$($(GOLANGCI_LINT) version 2>/dev/null || true)"; \ case "$$installed_version" in \ *"version $(GOLANGCI_LINT_VERSION) "*) ;; \ *) echo "Installing golangci-lint v$(GOLANGCI_LINT_VERSION)..."; \ GOBIN="$(dir $(GOLANGCI_LINT))" go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$(GOLANGCI_LINT_VERSION) ;; \ esac @command -v pre-commit > /dev/null 2>&1 || (echo "Installing pre-commit..." && pip install pre-commit==4.5.1) @echo "All tools are ready" # Compiles the gateway. The dashboard must be built first (see `frontend`); # the binary embeds internal/admin/dashboard/static/dist. build: frontend go build -ldflags '$(LDFLAGS)' -o bin/gomodel ./cmd/gomodel # Run the application. # # Built and exec'd rather than `go run`: `go run` exits 1 when it is # interrupted, even after the program it supervises shuts down cleanly, so # Ctrl+C always ended in a bogus "make: *** [run] Error 1". exec replaces the # recipe shell, which also puts the gateway directly under make's signal # handling instead of behind a supervisor. run: go build -tags=swagger -ldflags '$(LDFLAGS)' -o bin/gomodel ./cmd/gomodel LOG_LEVEL="$(LOG_LEVEL)" SWAGGER_ENABLED="$(SWAGGER_ENABLED)" exec ./bin/gomodel # Seed the local SQLite database and start GoModel with a populated dashboard. # Guardrails (which imply plugins) are on so the seeded guardrail instances and # the workflows referencing them are live rather than capped off at runtime. # Audit retention is raised to the seeded window: the 30-day default would # delete two thirds of the demo audit log on the first startup sweep. # Exported so the seeder reads the same window as the retention settings: a # maintainer changing this default must not leave them out of step. # The allowlist mode serves the seeded providers' model lists without calling # upstream /models with their placeholder keys. export DEMO_DAYS ?= 90 demo: seed-demo-data $(MAKE) run GOMODEL_DEMO_MODE=true GUARDRAILS_ENABLED=true \ CONFIGURED_PROVIDER_MODELS_MODE=allowlist \ LOGGING_RETENTION_DAYS=$(DEMO_DAYS) USAGE_RETENTION_DAYS=$(DEMO_DAYS) # Clean build artifacts clean: rm -rf bin/ # Tidy dependencies tidy: go mod tidy # Verify dependencies are tidy and match downloaded module checksums without # mutating the working tree. CI and release builds use this target. mod-check: go mod tidy -diff go mod verify # Docker Compose: Redis, PostgreSQL, MongoDB, Adminer (no app image build) infra: docker compose up -d # Docker Compose: full stack (GoModel + Prometheus; builds app image when needed) image: frontend docker compose --profile app up -d # Shared-object plugin support (Go's plugin package) needs a cgo-enabled # binary; the default `build` and the release binaries are static. These # targets produce the cgo variants. Plugins must be built with the same Go # toolchain and flags as the binary that loads them: `gomodel plugin build` # copies the flags of the gomodel binary that runs it. build-plugins: frontend CGO_ENABLED=1 go build -ldflags '$(LDFLAGS)' -o bin/gomodel-plugins ./cmd/gomodel # Docker image with plugin support (Dockerfile.plugins). Tag: gomodel:-plugins. image-plugins: frontend docker build -f Dockerfile.plugins -t gomodel:$(VERSION)-plugins -t gomodel:plugins \ --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) --build-arg DATE=$(DATE) . # Build every example plugin under docs/example_plugins into ./plugins/.so # using the toolchain of this checkout (matches `make build-plugins`). example-plugins: @mkdir -p plugins @for dir in docs/example_plugins/*/; do \ name=$$(basename $$dir); \ echo "building $$dir -> plugins/$$name.so"; \ go run ./cmd/gomodel plugin build -o plugins/$$name.so $$dir || exit 1; \ done # Seed rolling demo telemetry and dashboard configuration into SQLite. # Usage: SQLITE_PATH=data/gomodel.db make seed-demo-data seed-demo-data: bash tools/seed-demo-data.sh # Run unit tests only test: frontend-check go test ./cmd/... ./config/... ./ext/... ./internal/... ./run/... -v # Run unit tests with race detection and coverage test-race: frontend-check go test -v -race -coverprofile=coverage.out ./cmd/... ./config/... ./ext/... ./internal/... ./run/... # Build the Svelte dashboard into internal/admin/dashboard/static/dist, which # the Go binary embeds. The output is not committed: CI builds it in a # secretless job (docs/adr/0010-dashboard-built-in-ci.md). --ignore-scripts # keeps npm lifecycle scripts from running; the build does not need them. frontend: cd web/dashboard && npm ci --no-audit --no-fund --ignore-scripts && npm run build # The Go suites embed the dashboard, so they need a build in place. This only # checks; it does not build, because CI supplies static/dist as an artifact # from the secretless `frontend` job and must not rebuild it here. frontend-check: @test -f internal/admin/dashboard/static/dist/index.html || { \ echo "internal/admin/dashboard/static/dist is missing: run 'make frontend' first." >&2; exit 1; } # Write a placeholder bundle so the Go suites compile and pass without Node. # CI test jobs use this instead of waiting for the `frontend` job; the Build # job and releases still embed the real bundle. A no-op when a bundle exists. frontend-stub: sh tools/frontend-stub.sh # Run dashboard JavaScript unit tests test-dashboard: cd web/dashboard && npm test # Run e2e tests (uses an in-process mock LLM server; no Docker required) test-e2e: frontend-check go test -v -tags=e2e ./tests/e2e/... # Run integration tests (requires Docker) test-integration: go test -v -tags=integration -timeout=10m ./tests/integration/... # Run contract tests (validates API response structures against golden files) test-contract: go test -v -tags=contract -timeout=5m ./tests/contract/... # Live provider tests call real upstreams and spend quota. Each one skips unless # its credential is set, e.g. CHATGPT_API_KEY for the ChatGPT prompt-cache check. test-live: go test -count=1 -v -tags=live -timeout=10m -run TestLive ./internal/providers/... # Run all tests including dashboard, e2e, integration, and contract tests test-all: test test-dashboard test-e2e test-integration test-contract # Every guard in tests/perf, with no -run filter: the name list this used to # carry had to be edited by hand for each new guard, and silently left the ones # nobody remembered to add (TestFormatPerfGuardResult, # TestSessionIDVisibilityByBodySize) running nowhere. A guard that CI does not # execute is worse than no guard, because it reads as covered. perf-check: go test -count=1 -v ./tests/perf/... perf-bench: go test -bench=. -benchmem ./tests/perf/... # Run each fuzz target for FUZZTIME. The seed corpora already run as plain # tests under `make test`; this explores beyond them and is not part of CI. # Usage: make fuzz FUZZTIME=5m FUZZTIME ?= 30s fuzz: go test -run='^$$' -fuzz='^FuzzRequestDecompression$$' -fuzztime=$(FUZZTIME) ./internal/server go test -run='^$$' -fuzz='^FuzzRewriteBatchSource$$' -fuzztime=$(FUZZTIME) ./internal/core go test -run='^$$' -fuzz='^FuzzUnknownJSONFieldsRoundTrip$$' -fuzztime=$(FUZZTIME) ./internal/core # Record API responses for contract tests # Usage: OPENAI_API_KEY=sk-xxx make record-api record-api: @echo "Recording OpenAI chat completion..." go run ./cmd/recordapi -provider=openai -endpoint=chat \ -output=tests/contract/testdata/openai/chat_completion.json @echo "Recording OpenAI models..." go run ./cmd/recordapi -provider=openai -endpoint=models \ -output=tests/contract/testdata/openai/models.json @echo "Done! Golden files saved to tests/contract/testdata/" swagger: go run github.com/swaggo/swag/v2/cmd/swag init --generalInfo main.go \ --dir cmd/gomodel,internal \ --output cmd/gomodel/docs \ --outputTypes go \ --parseDependency @command -v node >/dev/null 2>&1 || { echo "node is required to build docs; install from https://nodejs.org" >&2; exit 1; } node tools/swagger-postprocess.mjs cmd/gomodel/docs/docs.go $(MAKE) docs-openapi docs-openapi: @command -v node >/dev/null 2>&1 || { echo "node is required to build docs; install from https://nodejs.org" >&2; exit 1; } @command -v npx >/dev/null 2>&1 || { echo "npx is required; install npm (includes npx)" >&2; exit 1; } @tmp_dir=$$(mktemp -d); \ trap 'rm -rf "$$tmp_dir"' EXIT; \ go run github.com/swaggo/swag/v2/cmd/swag init --quiet --generalInfo main.go \ --dir cmd/gomodel,internal \ --output "$$tmp_dir" \ --outputTypes json \ --parseDependency; \ npx -y swagger2openapi@7.0.8 --patch -o docs/openapi.json "$$tmp_dir/swagger.json"; \ DOCS_API_SERVERS="$(DOCS_API_SERVERS)" node tools/openapi-postprocess.mjs docs/openapi.json # Run linter lint: $(GOLANGCI_LINT) run --build-tags=$(BUILD_TAGS) ./cmd/... ./config/... ./ext/... ./internal/... ./run/... ./tests/... # Lint the Helm chart with every CI values profile and validate the rendered # manifests against the Kubernetes schemas (requires helm and kubeconform). helm-lint: helm lint --strict helm for values in helm/ci/*-values.yaml; do helm lint --strict helm -f "$$values"; done rendered=$$(mktemp); trap 'rm -f "$$rendered"' EXIT; \ for values in helm/ci/*-values.yaml; do \ helm template gomodel helm -f "$$values" --namespace gomodel > "$$rendered" || exit 1; \ kubeconform -strict -summary -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' < "$$rendered" || exit 1; \ done # Run linter with auto-fix. Mirrors `lint`: same tags, same packages, so the # autofix pass cannot silently skip the tag-gated files under tests/. lint-fix: $(GOLANGCI_LINT) run --fix --build-tags=$(BUILD_TAGS) ./cmd/... ./config/... ./ext/... ./internal/... ./run/... ./tests/... # Report modernizations go fix would apply, without touching the tree. # Exits non-zero when the tree has drifted; run `make fix` to apply. fix-check: go fix -diff -tags=$(BUILD_TAGS) ./... # Apply go fix modernizations in place. # # Not idempotent in one pass: when go fix marks a helper `//go:fix inline` it # inlines the callers on the *next* run, which can leave the helper orphaned. # Re-run until `make fix-check` is clean, then delete any helper `make lint` # now reports as unused. fix: go fix -tags=$(BUILD_TAGS) ./...