.DEFAULT_GOAL := help SHELL := /bin/bash # Load .env if present so targets can use the same variables as compose. ifneq (,$(wildcard ./.env)) include .env export endif # === Container engine detection (docker OR podman) ============================ # # Auto-detects on PATH, with docker preferred when both are installed. # Override either variable from the environment or the command line: # make up ENGINE=podman # make up COMPOSE="podman compose" # ENGINE ?= $(shell \ if command -v docker >/dev/null 2>&1; then echo docker; \ elif command -v podman >/dev/null 2>&1; then echo podman; \ fi) ifeq ($(strip $(ENGINE)),) $(error Neither docker nor podman found in PATH. Install one, or set ENGINE=...) endif # Pick the right compose driver based on the chosen engine. # - docker → "docker compose" (compose v2 plugin) # - podman 4.0+ → "podman compose" # - older podman → "podman-compose" (legacy Python wrapper) ifndef COMPOSE ifeq ($(ENGINE),docker) COMPOSE := docker compose else COMPOSE := $(shell \ if podman compose version >/dev/null 2>&1; then echo "podman compose"; \ elif command -v podman-compose >/dev/null 2>&1; then echo "podman-compose"; \ else echo "podman compose"; fi) endif endif # Project name is mostly used to look up volumes / containers via raw engine calls. PROJECT ?= metamorph # Suppress the noisy `>>>> Executing external compose provider …` banner that # `podman compose` emits on every invocation (harmless, but spammy in logs). export PODMAN_COMPOSE_WARNING_LOGS = false .PHONY: help env engine up down build rebuild logs logs-api ps health shell-api shell-db psql \ dev dev-api dev-front lint lint-api lint-front fmt test test-api test-front \ e2e e2e-install e2e-report e2e-up wait-healthy \ migrate migrate-down migrate-revision migrate-status \ seed-mitre print-install-token print-install-token-force \ volumes inspect-health clean help: ## Show this help @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_0-9-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @printf "\n Container engine in use: \033[33m%s\033[0m | compose: \033[33m%s\033[0m\n" "$(ENGINE)" "$(COMPOSE)" engine: ## Print the detected container engine and compose driver @echo "ENGINE=$(ENGINE)" @echo "COMPOSE=$(COMPOSE)" env: ## Bootstrap a local .env from .env.example if missing @test -f .env || (cp .env.example .env && echo "Created .env — edit it before 'make up'") # === Compose lifecycle ======================================================== up: env ## Build (if needed) and start all services $(COMPOSE) up -d --build down: ## Stop and remove containers (keep volumes) $(COMPOSE) down build: ## Build images without starting $(COMPOSE) build rebuild: ## Force rebuild without cache $(COMPOSE) build --no-cache logs: ## Tail logs from all services $(COMPOSE) logs -f --tail=200 logs-api: ## Tail only the api container logs (useful to inspect JSON log lines) $(COMPOSE) logs -f --tail=200 api ps: ## List running services $(COMPOSE) ps shell-api: ## Shell into the api container $(COMPOSE) exec api bash shell-db: ## Shell into the db container $(COMPOSE) exec db sh psql: ## Open psql in the db container $(COMPOSE) exec db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) # === Container introspection (engine-agnostic) ================================ volumes: ## List the named volumes created by this project @$(ENGINE) volume ls --filter "name=$(PROJECT)_" inspect-health: ## Print the health status of every container in the project @for c in $(PROJECT)-db $(PROJECT)-api $(PROJECT)-front; do \ printf "%-30s " "$$c"; \ $(ENGINE) inspect --format '{{.State.Health.Status}}' "$$c" 2>/dev/null || echo "(no-healthcheck or stopped)"; \ done # === Local dev (no container) ================================================= dev: ## Run api + front locally in parallel (Ctrl-C stops both) @$(MAKE) -j2 --no-print-directory dev-api dev-front dev-api: ## Run Flask in dev mode cd backend && APP_ENV=dev uv run flask --app app.main run --debug --host 0.0.0.0 --port 8000 dev-front: ## Run Vite dev server cd frontend && npm run dev # === Quality ================================================================== lint: lint-api lint-front ## Lint everything lint-api: cd backend && uv run ruff check . && uv run ruff format --check . lint-front: cd frontend && npm run lint && npm run typecheck fmt: ## Auto-format cd backend && uv run ruff format . cd frontend && npm run format test: test-api test-front ## Run all tests test-api: ## Run backend pytest in an ephemeral container against the live DB @echo "Building backend test image (target: test)…" @$(ENGINE) build -q --target test -t metamorph-api-test ./backend > /dev/null @echo "Running pytest…" $(ENGINE) run --rm \ --network $(PROJECT)_metamorph \ -e APP_ENV=test \ -e POSTGRES_DB=$(POSTGRES_DB) \ -e POSTGRES_USER=$(POSTGRES_USER) \ -e POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) \ -e POSTGRES_HOST=db \ -e POSTGRES_PORT=5432 \ -e JWT_SECRET=test-only-secret-not-checked-in-this-mode \ -e LOG_LEVEL=WARNING \ -e FRONT_ORIGIN=http://localhost:8080 \ metamorph-api-test test-front: cd frontend && npm test --if-present # === End-to-end tests (Playwright) ============================================ e2e-install: ## Install Playwright deps + chromium browser (use sudo on Debian/Ubuntu) cd e2e && npm install && npx playwright install --with-deps chromium e2e: wait-healthy ## Run the e2e suite against the running stack cd e2e && BASE_URL=http://localhost:$(or $(HOST_FRONT_PORT),8080) npm test e2e-report: ## Open the latest Playwright HTML report cd e2e && npx playwright show-report e2e-up: ## Bring the stack up, wait healthy, then run e2e $(MAKE) up $(MAKE) e2e health: ## Curl the health endpoint via the front nginx (one shot) @curl -sSf "http://localhost:$(or $(HOST_FRONT_PORT),8080)/api/v1/health" \ && echo "" \ || (echo " unreachable — is 'make up' done?"; exit 1) wait-healthy: ## Wait until the front+api are reachable (60s timeout) @port=$(or $(HOST_FRONT_PORT),8080); \ echo "Waiting for http://localhost:$$port/api/v1/health …"; \ for i in $$(seq 1 30); do \ if curl -sf "http://localhost:$$port/api/v1/health" > /dev/null; then \ echo " ready after $$((i*2))s"; exit 0; \ fi; \ sleep 2; \ done; \ echo " timeout after 60s — check 'make ps' and 'make logs'"; exit 1 # === App-specific commands (placeholders for later milestones) ================ migrate: ## Apply DB migrations (alembic upgrade head, runs inside the api container) $(COMPOSE) exec api alembic upgrade head migrate-down: ## Roll back the latest migration $(COMPOSE) exec api alembic downgrade -1 migrate-revision: ## Generate a new autogenerated migration: make migrate-revision MSG="my message" @test -n "$(MSG)" || (echo "Usage: make migrate-revision MSG=\"short description\""; exit 1) $(COMPOSE) exec api alembic revision --autogenerate -m "$(MSG)" migrate-status: ## Show current revision and any pending migrations $(COMPOSE) exec api alembic current @echo "---" $(COMPOSE) exec api alembic heads seed-mitre: ## Seed MITRE ATT&CK Enterprise dataset (M4) $(COMPOSE) exec api flask --app app.cli metamorph seed-mitre print-install-token: ## Print the bootstrap install token (M2) $(COMPOSE) exec api flask --app app.cli metamorph print-install-token print-install-token-force: ## Force-mint a fresh install token (M2, --force) $(COMPOSE) exec api flask --app app.cli metamorph print-install-token --force clean: ## Remove containers, networks AND volumes (DESTRUCTIVE) $(COMPOSE) down -v