run_turn takes answer_tools: false ends the turn once the tools have
answered. The assistant node uses it and says what was set ("Kitchen light
off, living room light off."), which saves one model round, about 1.1 s on
the P100. A turn without a call still streams the model's own words.
rebuild-frontend keeps the running stack's FRONTEND_HOST, so the token
issuer no longer flips between the LAN address and app.localhost.
292 lines
16 KiB
Makefile
292 lines
16 KiB
Makefile
# ─── Fluksio app Makefile ───
|
|
# Convenience targets for development, testing, linting, and deployment.
|
|
# The workspace root delegates to these (see ../Makefile).
|
|
|
|
.PHONY: init env-check dev-utils dev dev-local dev-lan rebuild-frontend up down update install dev-backend dev-frontend \
|
|
generate-client sync-example test test-backend test-checks test-stack test-frontend test-webkit soak \
|
|
bench-startup bench-engine lint lint-backend lint-frontend format-frontend umami build docs docs-serve clean help
|
|
|
|
COMPOSE_ROOT := $(CURDIR)
|
|
# Explicit project name keeps this stack isolated from the sibling website
|
|
# stack (otherwise both default to "docker", the directory their compose
|
|
# files live in).
|
|
COMPOSE_PROJECT := fluksio-app
|
|
# Compose interpolation needs the project-local .env before reading compose.yml.
|
|
COMPOSE := docker compose -p $(COMPOSE_PROJECT) --env-file $(COMPOSE_ROOT)/.env
|
|
COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml
|
|
# Production also runs autoheal, which restarts the backend when its deep
|
|
# health check fails. It is profile-gated because it mounts the Docker socket.
|
|
COMPOSE_PROD_RUN := $(COMPOSE_PROD) --profile autoheal
|
|
# Host port `make dev-lan` publishes the app on.
|
|
APP_PORT ?= 8080
|
|
COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml
|
|
# Integrated local stack: dev stack wired onto the shared `proxy` network.
|
|
COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml
|
|
# Same stack, with the app also published on APP_PORT, for clients that cannot
|
|
# resolve app.$(DOMAIN).
|
|
COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.yml
|
|
# The frontend suite's own stack, a project of its own behind the local Traefik
|
|
# (compose.test.yml). The hostname is fixed, never read from a file or the
|
|
# command line, so the suite can only ever reach this stack.
|
|
TEST_DOMAIN := test.localhost
|
|
COMPOSE_TEST := DOMAIN=$(TEST_DOMAIN) STACK_NAME=fluksio-test TAG=test \
|
|
docker compose -p fluksio-test --env-file $(COMPOSE_ROOT)/.env -f docker/compose.yml \
|
|
-f docker/compose.dev.yml -f docker/compose.local.yml -f docker/compose.test.yml
|
|
|
|
help: ## Show available targets
|
|
@awk 'BEGIN{FS=":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
|
|
|
# ── Development (Docker) ──────────────────────────────────────────
|
|
|
|
# Hostname the local stack is served under. Never .env's DOMAIN: a checkout
|
|
# configured for a deployment carries that deployment's domain, and a local
|
|
# stack started under it answers to the same names the live instance does.
|
|
# Target-specific on purpose — an exported DOMAIN outranks --env-file in
|
|
# compose interpolation, which would put the *production* targets on localhost.
|
|
# `make dev DOMAIN=fluksio.com` still wins.
|
|
dev dev-utils dev-local dev-lan rebuild-frontend: export DOMAIN ?= localhost
|
|
|
|
# A key the template lists and the compose files default, missing from .env,
|
|
# falls back without a word. For TZ that means every cron runs on UTC. Only a
|
|
# warning, so a start never fails over it. `update` runs it after its pull.
|
|
dev dev-local dev-lan up: env-check
|
|
env-check:
|
|
@missing=$$(grep -ohE '\$$\{[A-Z_]+:?-' docker/compose*.yml | sed 's/^..//; s/:\{0,1\}-$$//' | sort -u \
|
|
| while read -r k; do \
|
|
grep -q "^$$k=" .env.example && ! grep -q "^$$k=" .env && printf ' %s' "$$k"; \
|
|
done); \
|
|
[ -z "$$missing" ] || echo " ! .env lacks$$missing, so compose uses its defaults. See .env.example."
|
|
|
|
# First-time bootstrap. Idempotent: only secrets still set to `changethis` are
|
|
# generated, so an existing .env keeps its values.
|
|
init: ## First-time setup (.env + secrets + docker network)
|
|
@[ -f .env ] || { cp .env.example .env && echo " + .env created from .env.example"; }
|
|
@for k in SECRET_KEY POSTGRES_PASSWORD FIRST_SUPERUSER_PASSWORD; do \
|
|
if grep -q "^$$k=changethis$$" .env; then \
|
|
sed -i "s/^$$k=changethis$$/$$k=$$(openssl rand -hex 16)/" .env && echo " + $$k generated"; \
|
|
fi; \
|
|
done
|
|
@docker network inspect proxy >/dev/null 2>&1 || docker network create proxy >/dev/null
|
|
@echo ""
|
|
@echo " Admin login: $$(sed -n 's/^FIRST_SUPERUSER=//p' .env) / $$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' .env)"
|
|
@echo " Next: 'make dev-local', then open http://app.localhost"
|
|
|
|
dev-utils: ## Start only the utility containers (proxy, mailcatcher)
|
|
$(COMPOSE_DEV) up --build proxy mailcatcher
|
|
|
|
dev: ## Start the full dev stack (includes a local Traefik proxy)
|
|
$(COMPOSE_DEV) up --build
|
|
|
|
dev-local: ## Start the local stack at http://app.localhost (also run by the root `make dev`)
|
|
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \
|
|
$(COMPOSE_LOCAL) up --build -d --remove-orphans proxy backend docs mailcatcher
|
|
|
|
# The host's LAN address is the source of its default route. Without `ip`
|
|
# (macOS) it stays empty and the instance keeps calling itself app.$(DOMAIN).
|
|
dev-lan: ## Same, plus the app on http://<host-ip>:$(APP_PORT) (no DNS needed)
|
|
@ip=$$(ip route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p'); \
|
|
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} APP_PORT=$(APP_PORT) \
|
|
LAN_URL=$${ip:+http://$$ip:$(APP_PORT)} \
|
|
$(COMPOSE_LAN) up --build -d --remove-orphans proxy backend mailcatcher
|
|
|
|
# The dashboard is built into the app image, so a UI change needs a rebuild;
|
|
# only the image's frontend stage runs again. The domain and the overlay set
|
|
# come off the *running* stack rather than from a file, so the rebuilt
|
|
# container keeps the hostname and, on a LAN stack, the published port it was
|
|
# started with. A published host port is how the lan overlay is recognised,
|
|
# because it is the thing only that overlay adds. Its address is kept too,
|
|
# since it is the issuer worker and agent tokens were signed for.
|
|
rebuild-frontend: ## Rebuild the app image and restart it (after a UI change)
|
|
@domain=$$(docker inspect fluksio-api 2>/dev/null \
|
|
| grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \
|
|
port=$$(docker inspect fluksio-api \
|
|
--format '{{range $$p, $$c := .NetworkSettings.Ports}}{{range $$c}}{{.HostPort}} {{end}}{{end}}' \
|
|
2>/dev/null | awk '{print $$1}'); \
|
|
lan=$$(docker inspect fluksio-api --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
|
2>/dev/null | sed -n 's/^FRONTEND_HOST=//p'); \
|
|
if [ -n "$$port" ]; then \
|
|
echo " lan stack detected (published on $$port) — keeping compose.lan.yml"; \
|
|
compose="$(COMPOSE_LAN)"; \
|
|
else \
|
|
compose="$(COMPOSE_LOCAL)"; \
|
|
fi; \
|
|
DOMAIN=$${domain:-$$DOMAIN} ENVIRONMENT=$${ENVIRONMENT:-local} \
|
|
APP_PORT=$${port:-$(APP_PORT)} LAN_URL=$$lan \
|
|
$$compose up --build -d backend
|
|
|
|
up: ## Start the production stack
|
|
$(COMPOSE_PROD_RUN) up --build -d
|
|
|
|
umami: ## Provision + start the optional Umami site-analytics service (idempotent)
|
|
# Guard: the dashboard is internet-facing on analytics.$(DOMAIN); refuse to
|
|
# start when its session-signing secret is unset or still the placeholder.
|
|
@secret=$$(grep -E '^UMAMI_APP_SECRET=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
|
|
if [ -z "$$secret" ] || [ "$$secret" = "changethis" ]; then \
|
|
echo "ERROR: UMAMI_APP_SECRET is empty or 'changethis' in .env — run the workspace root's scripts/setup.sh (or set it: openssl rand -hex 32) before 'make umami'." >&2; \
|
|
exit 1; \
|
|
fi
|
|
# --no-recreate: reuse an already-running db instead of recreating it under
|
|
# the prod compose set, which would blip every service that depends on it.
|
|
$(COMPOSE_PROD) up -d --wait --no-recreate db
|
|
# Least-privilege role owning only the umami database, so the third-party
|
|
# image never holds the shared superuser credentials.
|
|
@pguser=$$(grep -E '^POSTGRES_USER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); pguser=$${pguser:-postgres}; \
|
|
role=$$(grep -E '^UMAMI_DB_USER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); role=$${role:-umami}; \
|
|
pw=$$(grep -E '^UMAMI_DB_PASSWORD=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
|
|
db=$$(grep -E '^UMAMI_DB=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); db=$${db:-umami}; \
|
|
if [ -z "$$pw" ] || [ "$$pw" = "changethis" ]; then \
|
|
echo "ERROR: UMAMI_DB_PASSWORD is empty or 'changethis' in .env — run the workspace root's scripts/setup.sh before 'make umami'." >&2; \
|
|
exit 1; \
|
|
fi; \
|
|
$(COMPOSE_PROD) exec -T db psql -v ON_ERROR_STOP=1 -U "$$pguser" -d postgres \
|
|
-v role="$$role" -v pw="$$pw" -v db="$$db" < docker/provision-umami-db.sql
|
|
# --no-deps: only (re)create umami, never restart the shared db underneath it.
|
|
$(COMPOSE_PROD) --profile analytics up -d --no-deps umami
|
|
|
|
update: ## Pull, rebuild using the layer cache, and recreate changed containers
|
|
git pull
|
|
@$(MAKE) --no-print-directory env-check
|
|
$(COMPOSE_PROD_RUN) build
|
|
$(COMPOSE_PROD_RUN) up -d --remove-orphans
|
|
docker image prune -f
|
|
|
|
down: ## Stop all running containers
|
|
-$(COMPOSE_LOCAL) down
|
|
-$(COMPOSE_TEST) down
|
|
-$(COMPOSE_PROD_RUN) down
|
|
|
|
# ── Development (local, no Docker) ───────────────────────────────
|
|
# Run `make dev-backend` and `make dev-frontend` in two separate terminals.
|
|
|
|
install: ## Install all dependencies (backend + frontend)
|
|
# Every extra: the suite exercises the connectors that moved into
|
|
# `fluksio[server]`, and strict mypy checks their call sites.
|
|
cd backend && uv sync --all-extras
|
|
cd frontend && bun install
|
|
|
|
dev-backend: ## Start the FastAPI backend with hot-reload (local)
|
|
cd backend && uv run --all-extras uvicorn fluksio.main:app --reload --ws-per-message-deflate false
|
|
|
|
dev-frontend: ## Start the Vite dev server (local)
|
|
cd frontend && bun dev
|
|
|
|
generate-client: ## Regenerate the frontend SDK from the backend's OpenAPI schema
|
|
bash scripts/generate-client.sh
|
|
|
|
sync-example: ## Upload `examples/myresearch` the way a data scientist would
|
|
# The same command the docs give, against whichever engine `fluksio login`
|
|
# last talked to. The nodes import the package from this checkout, so the
|
|
# engine has to be one that can see this path.
|
|
cd backend && uv run fluksio sync ../examples/myresearch
|
|
|
|
# ── Testing ───────────────────────────────────────────────────────
|
|
|
|
test: test-backend test-checks test-frontend ## Run all tests (backend + checks + frontend)
|
|
|
|
# The frontend has no unit-test runner, so a pure helper is checked by a
|
|
# `*.check.ts` script of plain `node:assert` that runs itself and needs no
|
|
# stack. This is the target that runs every one of them.
|
|
test-checks: ## Run the frontend's *.check.ts scripts
|
|
cd frontend && bun run check
|
|
|
|
test-backend: ## Run backend tests (pytest + coverage)
|
|
# Its own SQLite file in a temp directory (tests/__init__.py), so this needs
|
|
# nothing running and touches no development data.
|
|
cd backend && uv run --all-extras bash scripts/test.sh
|
|
|
|
PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/p' frontend/package.json | head -1)
|
|
|
|
# The suite creates and deletes flows, dashboards and users, so it runs against
|
|
# a stack of its own, never the dev instance. `up` only recreates what changed,
|
|
# and the stack is left running for the next run (and for a look at a failure
|
|
# on http://app.test.localhost). `make down` stops it. Its volumes are
|
|
# expendable: `docker compose -p fluksio-test down -v` starts it over.
|
|
test-stack: ## Start or update the frontend suite's own stack at http://app.test.localhost
|
|
@docker ps -q -f name='^fluksio-app-proxy-1$$' | grep -q . \
|
|
|| { echo " ✗ the local Traefik is not running, 'make dev' first"; exit 1; }
|
|
$(COMPOSE_TEST) up --build -d --wait --remove-orphans redis backend
|
|
|
|
# The Playwright container, minus its network. tests/guard.ts refuses a public
|
|
# address on top of the fixed hostname.
|
|
PW_RUN = docker run --rm --ipc=host --user $$(id -u):$$(id -g) -e HOME=/tmp \
|
|
-v $(COMPOSE_ROOT):/app -w /app/frontend \
|
|
-e PLAYWRIGHT_BASE_URL=http://app.$(TEST_DOMAIN) \
|
|
-e CI=$${CI:-1} \
|
|
-e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \
|
|
-e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)"
|
|
PW_IMAGE = mcr.microsoft.com/playwright:v$(PW_VERSION)-noble
|
|
|
|
# The names are mapped onto the local Traefik by address, so DNS never decides.
|
|
# Twice, because Chromium pins *.localhost to loopback whatever /etc/hosts says.
|
|
test-frontend: test-stack ## Run frontend tests (Playwright e2e) against the test stack
|
|
@ip=$$(docker network inspect proxy \
|
|
--format '{{range .Containers}}{{if eq .Name "fluksio-app-proxy-1"}}{{.IPv4Address}}{{end}}{{end}}' \
|
|
2>/dev/null | cut -d/ -f1); \
|
|
[ -n "$$ip" ] || { echo " ✗ the local Traefik is not on the proxy network"; exit 1; }; \
|
|
$(PW_RUN) --network proxy \
|
|
--add-host app.$(TEST_DOMAIN):$$ip \
|
|
-e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip" \
|
|
$(PW_IMAGE) npx playwright test $(PLAYWRIGHT_ARGS)
|
|
|
|
# WebKit takes *.localhost to loopback through GLib and ignores /etc/hosts, so
|
|
# this run shares Traefik's network namespace, where loopback *is* Traefik.
|
|
# Only the drag specs, which is where the engines differ.
|
|
test-webkit: test-stack ## Run the drag specs in WebKit against the test stack
|
|
@$(PW_RUN) --network container:fluksio-app-proxy-1 -e PLAYWRIGHT_WEBKIT=1 \
|
|
$(PW_IMAGE) npx playwright test --project=webkit $(PLAYWRIGHT_ARGS)
|
|
|
|
# Load and chaos against a *running* stack, never part of `make test`: it
|
|
# restarts this stack's containers. `SOAK_ARGS="--dry-run"` only looks.
|
|
soak: ## Run the soak/chaos harness (SOAK_ARGS="--minutes 30 --scenario redis --force")
|
|
cd backend && uv run python scripts/soak.py $(SOAK_ARGS)
|
|
|
|
bench-startup: ## Time submitting a run (BENCH_ARGS="--kedro ../some/kedro/project")
|
|
cd backend && uv run python scripts/bench_startup.py $(BENCH_ARGS)
|
|
|
|
# In-process, so it needs no stack: what a message costs the engine in work
|
|
# and in time. `BENCH_ARGS="--redis <host>"` measures it against a real Redis,
|
|
# which is where the round trips are.
|
|
bench-engine: ## Engine throughput and per-message cost (BENCH_ARGS="--redis localhost")
|
|
cd backend && uv run python scripts/bench_engine.py $(BENCH_ARGS)
|
|
|
|
# ── Linting ───────────────────────────────────────────────────────
|
|
|
|
build: ## Build the fluksio and fluksio-worker wheels into dist/
|
|
uv build --all-packages --out-dir dist
|
|
|
|
lint: lint-backend lint-frontend ## Run all linters
|
|
|
|
lint-backend: ## Lint backend with ruff + mypy
|
|
cd backend && uv run --all-extras ruff check .
|
|
cd backend && uv run --all-extras ruff format --check .
|
|
cd backend && uv run --all-extras mypy fluksio
|
|
cd worker && uv run --no-project --with mypy mypy fluksio_worker
|
|
|
|
lint-frontend: ## Lint frontend with biome
|
|
cd frontend && bun run lint
|
|
|
|
format-frontend: ## Apply biome's fixes to the frontend (what `lint-frontend` only reports)
|
|
cd frontend && bun run format
|
|
|
|
# ── Documentation ─────────────────────────────────────────────────
|
|
# The public site on docs.${DOMAIN}, served by the `docs` service in
|
|
# docker/compose.yml. These targets are for local authoring: zensical runs on
|
|
# demand via uvx, so it never touches the backend environment. Pinned to the
|
|
# version docker/Dockerfile.docs and .gitea/workflows/docs.yml ship, so local
|
|
# authoring builds with what docs.fluksio.com actually gets.
|
|
ZENSICAL := zensical==0.0.46
|
|
|
|
docs: ## Build the documentation site into ./site
|
|
uvx $(ZENSICAL) build --clean
|
|
|
|
docs-serve: ## Serve the documentation site locally with live reload
|
|
uvx $(ZENSICAL) serve
|
|
|
|
# ── Cleanup ───────────────────────────────────────────────────────
|
|
|
|
clean: ## Remove build artifacts and caches
|
|
rm -rf frontend/dist frontend/blob-report frontend/test-results
|
|
rm -rf backend/.pytest_cache backend/htmlcov site \
|
|
|| echo " ! not all removed (htmlcov/ root-owned? sudo rm -rf backend/htmlcov)" >&2
|
|
find backend -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|