Files
app/Makefile
T
stroblme 4c4920bda8
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m14s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 2m2s
Test Backend / test-backend (push) Failing after 2m31s
Compose Smoke Test / test-compose (push) Failing after 10s
Playwright Tests / merge-reports (push) Failing after 2m22s
Keep the lan overlay when the frontend is rebuilt
`rebuild-frontend` already took the domain off the running stack, for exactly
the right reason — `VITE_API_URL` is baked in at build time, so rebuilding
under a different one leaves the SPA calling an API that answers elsewhere. It
then dropped `compose.lan.yml` regardless of whether the stack had been started
with it, which is the same mistake with a worse blast radius: that overlay is
what publishes the host port and what builds with an *empty* VITE_API_URL, and
it is the only way a wall panel reaches the app at all, since a screen on the
LAN cannot resolve app.${DOMAIN}. Rebuilding without it unpublished the port
and baked in a name that device cannot resolve — the panel went dark for an
hour and it took a log to see why.

Recognised by the published host port, because that is the one thing only that
overlay adds. Same shape as the domain sniffing above it: what is running is
the authority, not what happens to be typed on the command line.
2026-08-30 15:07:15 +02:00

233 lines
12 KiB
Makefile

# ─── Fluksio app Makefile ───
# Convenience targets for development, testing, linting, and deployment.
# The workspace root delegates to these (see ../Makefile).
.PHONY: 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-frontend soak bench-startup 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 frontend 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 frontend also published on APP_PORT and proxying the API
# there, for clients that cannot resolve app.$(DOMAIN).
COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.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 installation 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
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 integrated local stack (called by the root `make dev`)
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \
$(COMPOSE_LOCAL) up --build -d proxy backend frontend docs mailcatcher
dev-lan: ## Same, plus the app on http://<host-ip>:$(APP_PORT) (no DNS needed)
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} APP_PORT=$(APP_PORT) \
$(COMPOSE_LAN) up --build -d proxy backend frontend mailcatcher
# The frontend is an nginx image, so a UI change needs a rebuild. Both the
# domain and the overlay set come off the *running* stack rather than from a
# file: VITE_API_URL is baked in at build time, so rebuilding under a different
# domain — or without the lan overlay a stack was started with — leaves the SPA
# calling an API that answers elsewhere. The lan overlay is what publishes the
# host port and builds with an empty VITE_API_URL, so dropping it takes a wall
# panel reaching http://<host-ip>:$(APP_PORT) off the air entirely and bakes a
# name it cannot resolve into the bundle. A published host port is how that
# overlay is recognised, because it is the thing only that overlay adds.
rebuild-frontend: ## Rebuild and restart the local frontend (after a UI change)
@domain=$$(docker inspect fluksio-app 2>/dev/null \
| grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \
port=$$(docker inspect fluksio-app \
--format '{{range $$p, $$c := .NetworkSettings.Ports}}{{range $$c}}{{.HostPort}} {{end}}{{end}}' \
2>/dev/null | awk '{print $$1}'); \
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)} \
$$compose up --build -d frontend
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
$(COMPOSE_PROD_RUN) build
$(COMPOSE_PROD_RUN) up -d --remove-orphans
docker image prune -f
down: ## Stop all running containers
-$(COMPOSE_LOCAL) 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 fastapi dev fluksio/main.py
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-frontend ## Run all tests (backend + frontend)
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)
# This suite creates and deletes flows, dashboards and users, so the hostname it
# is pointed at is not taken from any file: it is read off the running stack
# (the frontend container's own Traefik rule) and mapped onto the local Traefik
# by address, so DNS never decides. tests/guard.ts is the second line.
test-frontend: ## Run frontend tests (Playwright e2e) against the local 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 " ✗ proxy network or Traefik container not found — is the stack up?"; exit 1; }; \
domain=$$(docker inspect fluksio-app 2>/dev/null \
| grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \
[ -n "$$domain" ] || { echo " ✗ no running app stack to test — 'make dev' first"; exit 1; }; \
docker run --rm --network proxy --ipc=host \
--user $$(id -u):$$(id -g) -e HOME=/tmp \
--add-host app.$$domain:$$ip --add-host api.$$domain:$$ip \
-v $(COMPOSE_ROOT):/app -w /app/frontend \
-e PLAYWRIGHT_BASE_URL=http://app.$$domain \
-e PLAYWRIGHT_API_URL=http://api.$$domain \
-e HOST_RESOLVER_RULES="MAP app.$$domain $$ip, MAP api.$$domain $$ip" \
-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)" \
mcr.microsoft.com/playwright:v$(PW_VERSION)-noble \
npx playwright test $(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")
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
find backend -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true