# ============================================================================ # Krow backend — developer tasks # # Migration commands are thin wrappers over the golang-migrate CLI. The # migration FILES are the source of truth for the schema; nothing here or in # go-api/ ever issues DDL of its own. # # brew install golang-migrate # or see github.com/golang-migrate/migrate # ============================================================================ SHELL := /bin/bash .DEFAULT_GOAL := help # Capture APP_ENV as the process environment supplied it, BEFORE .env is # included. Make gives an included assignment precedence over the environment, # so without this `APP_ENV=production make migrate-down` would silently read # `development` out of .env and the guard below would not fire. APP_ENV_FROM_ENVIRONMENT := $(APP_ENV) ifneq (,$(wildcard .env)) include .env export endif # The environment wins; .env is only the fallback. A command-line override # (`make migrate-down APP_ENV=production`) outranks both, as Make intends. ifneq (,$(APP_ENV_FROM_ENVIRONMENT)) APP_ENV := $(APP_ENV_FROM_ENVIRONMENT) endif MIGRATIONS_DIR ?= ./migrations DATABASE_SCHEMA ?= public DATABASE_SSLMODE ?= disable # URL-encode the credentials and the database name. "Krow-force" is mixed-case # and hyphenated, and a password can contain anything; neither is safe raw. enc = $(shell printf '%s' '$(1)' | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read(), safe=""))') DB_URL = postgres://$(call enc,$(DATABASE_USER)):$(call enc,$(DATABASE_PASSWORD))@$(DATABASE_HOST):$(DATABASE_PORT)/$(call enc,$(DATABASE_NAME))?sslmode=$(DATABASE_SSLMODE)&search_path=$(DATABASE_SCHEMA) MIGRATE = migrate -path $(MIGRATIONS_DIR) -database "$(DB_URL)" PSQL = psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d "$(DATABASE_NAME)" .PHONY: help help: ## Show this help @grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' # ── Go ────────────────────────────────────────────────────────────────────── .PHONY: run run: ## Run the API against the local database cd go-api && go run ./cmd/api .PHONY: build build: ## Compile the API to go-api/bin/api cd go-api && go build -o bin/api ./cmd/api .PHONY: tidy tidy: ## go mod tidy cd go-api && go mod tidy .PHONY: fmt fmt: ## gofmt the module cd go-api && gofmt -w ./cmd ./internal .PHONY: vet vet: ## go vet the module cd go-api && go vet ./... .PHONY: test test: ## Run the Go tests cd go-api && go test ./... .PHONY: check check: fmt vet test ## Format, vet and test # ── Database ──────────────────────────────────────────────────────────────── .PHONY: db-create db-create: ## Create the database if it does not exist (never drops anything) @if psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d postgres -tAc \ "SELECT 1 FROM pg_database WHERE datname = '$(DATABASE_NAME)'" | grep -q 1; then \ echo "database \"$(DATABASE_NAME)\" already exists — nothing to do"; \ else \ psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d postgres -c \ 'CREATE DATABASE "$(DATABASE_NAME)"' && echo "created \"$(DATABASE_NAME)\""; \ fi .PHONY: seed seed: ## Load the frontend demo dataset (idempotent upsert in one transaction) cd go-api && SEED_FIXTURE_PATH=$(CURDIR)/seed/fixtures/seed.json go run ./cmd/seed .PHONY: gen-resources gen-resources: ## Regenerate domain descriptors from the live schema python3 scripts/gen_resources.py > go-api/internal/domain/resources_gen.go cd go-api && gofmt -w ./internal/domain .PHONY: db-health db-health: ## Ask the running API for its database health @curl -fsS http://$(HTTP_HOST):$(HTTP_PORT)/health | python3 -m json.tool # ── Migrations ────────────────────────────────────────────────────────────── .PHONY: migrate-up migrate-up: ## Apply all pending migrations $(MIGRATE) up .PHONY: migrate-down migrate-down: ## Roll back exactly one migration (guarded outside development) @if [ "$(APP_ENV)" != "development" ]; then \ echo "refusing: migrate-down is only permitted when APP_ENV=development (got '$(APP_ENV)')"; \ echo "roll back staging/production deliberately, with a reviewed plan and a fresh backup."; \ exit 1; \ fi $(MIGRATE) down 1 .PHONY: migrate-status migrate-status: ## Show the currently applied migration version @$(MIGRATE) version .PHONY: migrate-new migrate-new: ## Create a new migration pair: make migrate-new NAME=add_widgets @test -n "$(NAME)" || { echo "usage: make migrate-new NAME=add_widgets"; exit 1; } migrate create -ext sql -dir $(MIGRATIONS_DIR) -seq $(NAME) .PHONY: migrate-force migrate-force: ## Clear a dirty state: make migrate-force VERSION=1 @test -n "$(VERSION)" || { echo "usage: make migrate-force VERSION=1"; exit 1; } $(MIGRATE) force $(VERSION) # There is deliberately no `migrate-drop` target. golang-migrate's `drop` # command deletes every table in the database; it must never be one keystroke # away from a developer who meant `down`. Run it by hand if you truly need it. .PHONY: verify-schema verify-schema: ## Print the tables, enums, FKs and indexes that exist in the target schema @$(PSQL) -f scripts/verify_schema.sql # ── Docker ────────────────────────────────────────────────────────────────── # # These read infrastructure/.env, NOT the repository-root .env that `make run` # uses. Two files on purpose: the root one points at a developer's database # from the host, the infrastructure one points at a deployment's database from # inside a container, and DATABASE_HOST is different in each. COMPOSE_DIR := infrastructure COMPOSE_FILES := -f docker-compose.yml # Layer the local database in with: make docker-up LOCAL_DB=1 ifdef LOCAL_DB COMPOSE_FILES += -f docker-compose.local-db.yml endif COMPOSE = cd $(COMPOSE_DIR) && docker compose $(COMPOSE_FILES) .PHONY: docker-build docker-build: ## Build the API image $(COMPOSE) build .PHONY: docker-up docker-up: ## Start the stack (migrations run first). LOCAL_DB=1 adds PostgreSQL $(COMPOSE) up -d --build @echo "api: http://127.0.0.1:8080/health" @echo "logs: make docker-logs" .PHONY: docker-down docker-down: ## Stop the stack, keeping volumes $(COMPOSE) down .PHONY: docker-logs docker-logs: ## Follow the API logs $(COMPOSE) logs -f api .PHONY: docker-ps docker-ps: ## Show stack status and health $(COMPOSE) ps .PHONY: docker-migrate docker-migrate: ## Apply migrations only, without starting the API $(COMPOSE) run --rm migrate .PHONY: docker-seed docker-seed: ## Load the demo fixture into the deployment's database $(COMPOSE) run --rm --entrypoint /usr/local/bin/seed api .PHONY: docker-setpassword docker-setpassword: ## Set a password: make docker-setpassword EMAIL=demo@krow.app @test -n "$(EMAIL)" || { echo "usage: make docker-setpassword EMAIL=demo@krow.app"; exit 1; } $(COMPOSE) run --rm -it --entrypoint /usr/local/bin/setpassword api -email $(EMAIL) .PHONY: docker-config docker-config: ## Render the fully resolved compose configuration $(COMPOSE) config