# SPDX-FileCopyrightText: 2026 Latere AI # SPDX-License-Identifier: MIT GO ?= go # Image registry/namespace prefix — override for Nexus, ECR, Harbor, etc. # e.g. make push-images VERSION=v0.1.0 REGISTRY=123456789.dkr.ecr.eu-central-1.amazonaws.com/latere REGISTRY ?= ghcr.io/latere-ai .PHONY: check build test cover test-hermetic test-race validate deps cgo-free spec-lint fmt fmt-check hooks vet e2e images dist clean lint-modernize lint-config lint lint-otel build: $(GO) build ./... test: vet @go tool lateregate test # Coverage gate, per package rather than as a repository average: an # average lets a well-tested package carry an untested one and reports a # number nobody can act on. The threshold and any exemptions, each with # its reason, live in .lateregate.yaml. cover: $(GO) test ./... -coverprofile=coverage.out -coverpkg=./... @$(GO) tool lateregate cover -profile=coverage.out # Run the suite without the developer's PATH. # # Three CI failures in one day came from tests that depended on what # happened to be installed on the machine running them: `systemctl` # present-but-unprivileged on a runner and absent on macOS, and `claude` # on a developer's PATH and not on a runner's. Each passed locally and # failed in CI, which is the worst order to find out. # # Keeping only the Go toolchain and the system directories reproduces a # runner's environment closely enough to catch that class before a push. # The directories kept on PATH are named in .lateregate.yaml, so a test # that needs a system tool has to say so. test-hermetic: @$(GO) tool lateregate hermetic # The race detector needs cgo, which the shipped binaries do not: this is # about finding a race in the test harness, not about what we compile to. test-race: @go tool lateregate race # Validate all model manifests + deploy consistency (also run in `test` # via internal/deploycheck). The spec tree is a separate target now; see # spec-lint below. validate: deps cgo-free $(GO) run ./cmd/fornax validate models/ # The CLI's dependency footprint. A dependency arriving is not a failure; one # nobody decided on is, and the first symptom is otherwise a slower build. # The allowlist, whose value is the reason, lives in .lateregate.yaml. deps: @$(GO) tool lateregate depcheck # `make dist` builds with CGO_ENABLED=0 and docs/development.md says "no cgo". # A claim in a user-facing document with no gate behind it goes stale silently. # Reads source rather than the build, because a file can import "C" behind a # build tag this platform does not select and still be a violation. cgo-free: @$(GO) tool lateregate cgo-free # The spec tree checks: frontmatter, the closed status vocabulary, the index # rows, dependency edges and wikilinks. Conventions live in .lateregate.yaml. spec-lint: @$(GO) tool lateregate spec-lint # fmt formats all Go sources in place. fmt: gofmt -w . # fmt-check fails if any Go source is not gofmt-formatted. fmt-check: @$(GO) tool lateregate fmt-check # hooks installs the repository git hooks (pre-commit gofmt guard). hooks: git config core.hooksPath .githooks @echo "installed git hooks (core.hooksPath=.githooks)" vet: $(GO) vet ./... # lint-otel keeps outbound HTTP instrumented so traces propagate across # services. It fails on two shapes: an http.Client literal that sets no # Transport field, and any use of http.DefaultClient. The gate parses each # non-test Go file, so a Transport field several lines below the opening brace # still counts, a comment describing this rule does not trip it, and # Timeout: cfg.Transport.Timeout does not pass for a Transport field. lint-otel: @$(GO) tool lateregate otel-client # lint-modernize fails on code that a standard library call already covers. # It runs the toolchain modernizers, which overlap golangci-lint's modernize # linter but add three it does not carry: buildtag, hostport, and the # go:fix inline directives. newexpr and errorsastype are off for the reasons # recorded in .golangci.yml and named in .lateregate.yaml; lateregate checks # each still exists, because `go fix` rejects an unknown -name=false and the # gate would then pass silently. lint-modernize: @$(GO) tool lateregate modernize # .golangci.yml is generated and gitignored: golangci-lint cannot inherit a # shared config, so it is rendered from latere.ai/x/ci-gate on every run. # Regenerating rather than committing is what makes divergence impossible # instead of merely detectable. lint-config: @$(GO) tool lateregate golangci lint: lint-otel @go tool lateregate lint # CI-runnable e2e: full pull→push→verify and serve # serve→ready→metrics paths against fakes (no GPU, no network). # GPU e2e (make e2e-) is a release gate on real hardware. e2e: $(GO) test ./... -run 'E2E' -v images: docker build -f Dockerfile.sglang -t $(REGISTRY)/fornax-runtime-sglang:dev . docker build -f Dockerfile.sglang --build-arg SGLANG_IMAGE=lmsysorg/sglang:kimi-k3-c6ad1f26-20260729-amd64 -t $(REGISTRY)/fornax-runtime-sglang-k3:dev . docker build -f Dockerfile.vllm -t $(REGISTRY)/fornax-runtime-vllm:dev . docker build -f Dockerfile.mirror -t $(REGISTRY)/fornax-mirror:dev . # Versioned build + push of all four images (see docs/deploy.md). # Usage: make push-images VERSION=v0.1.0 [REGISTRY=...] # (requires docker login against $(REGISTRY)) # Named push-images rather than release: a release is a git tag with a # changelog section, and `release` is that command's name everywhere. push-images: @test -n "$(VERSION)" || { echo "usage: make push-images VERSION=vX.Y.Z [REGISTRY=...]"; exit 1; } docker build --platform linux/amd64 -f Dockerfile.sglang -t $(REGISTRY)/fornax-runtime-sglang:$(VERSION) . docker build --platform linux/amd64 -f Dockerfile.sglang --build-arg SGLANG_IMAGE=lmsysorg/sglang:kimi-k3-c6ad1f26-20260729-amd64 -t $(REGISTRY)/fornax-runtime-sglang-k3:$(VERSION) . docker build --platform linux/amd64 -f Dockerfile.vllm -t $(REGISTRY)/fornax-runtime-vllm:$(VERSION) . docker build --platform linux/amd64 -f Dockerfile.mirror -t $(REGISTRY)/fornax-mirror:$(VERSION) . docker push $(REGISTRY)/fornax-runtime-sglang:$(VERSION) docker push $(REGISTRY)/fornax-runtime-sglang-k3:$(VERSION) docker push $(REGISTRY)/fornax-runtime-vllm:$(VERSION) docker push $(REGISTRY)/fornax-mirror:$(VERSION) # Host binaries for the bare-metal deploy mode (specs/020, specs/024). # GOARCH must be set explicitly: a plain `go build` targets the builder, # so building on a laptop for an arm64 host would silently ship an amd64 # binary. CGO stays off so these cross-compile with no toolchain per # target. # # One binary per platform, not three: a host copied to by hand has one # file to place and one thing to ask for a version (specs/024). DIST_PLATFORMS = linux/amd64 linux/arm64 DIST_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo devel) DIST_COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo unknown) DIST_LDFLAGS = -s -w -X main.version=$(DIST_VERSION) -X main.commit=$(DIST_COMMIT) dist: @rm -rf dist @for p in $(DIST_PLATFORMS); do \ os=$${p%/*}; arch=$${p#*/}; \ echo "dist: $$os/$$arch $(DIST_VERSION)"; \ GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 $(GO) build \ -ldflags "$(DIST_LDFLAGS)" \ -o dist/$$os-$$arch/fornax ./cmd/fornax || exit 1; \ done @find dist -type f -exec ls -lh {} \; | awk '{print $$9, $$5}' clean: rm -f coverage.out rm -rf dist # Full local e2e: real small model + MinIO + mlx_lm engine (specs/011). # Needs Docker and uv; zero cloud credentials. ~1.5 GB one-time download. e2e-local: bash e2e/local/run.sh # The whole shared bar. Every gate lives in lateregate, pinned as a tool in # go.mod; this target is a name for `go tool lateregate` and nothing else. # The plan: `go tool lateregate list`. One gate: `go tool lateregate `. check: @go tool lateregate