Local work has been stranded on one machine: `make seed` replays seed/fixtures/seed.json, which is generated from krow-demo and has never reflected what is in a database. Agents published through importagents, knowledge ingested, applications screened — none of it had a path to another deployment. `make export-data` reads the database itself and writes replayable SQL. Three things it does deliberately: Tables are ordered topologically from pg_constraint rather than left in pg_dump's own order, which sorts by name and so fails on foreign keys in a way that depends on what the tables are called. Parents always precede children, and Kahn's algorithm breaks ties alphabetically so two runs against one schema produce a byte-identical file. Every statement is INSERT ... ON CONFLICT DO NOTHING. The export can create rows on a target and cannot modify or delete one. That is a property of the generated file, not a rule someone has to remember when they apply it. The file refuses to apply to a schema older than the one it came from. A restore into a half-migrated database half-succeeds, and a partial import is harder to unpick than a failed one. pg_dump 18 wraps its output in the psql meta-commands \restrict and \unrestrict. Dumping per table left the closing one without its opener, which fails with "not currently in restricted mode" and, under --single-transaction, rolls back having inserted nothing — silently, if the caller reads psql's output through a pipe instead of its exit code. Each table's slice is therefore cut at the last line ending in a semicolon, which no line of pg_dump's epilogue does and every generated statement does. sessions and schema_migrations are excluded: sessions are bound to cookies one deployment issued, and a stale schema_migrations row would make the target lie about its own version. Verified against a scratch database migrated to 15: all 25 tables match the source row for row, a second run is a no-op, and the guard refuses a version-10 target. The output is real tenant data — password hashes, personal details — so seed/exports/ is gitignored. It moves over scp, not through git. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
290 lines
12 KiB
Makefile
290 lines
12 KiB
Makefile
# ============================================================================
|
|
# 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)"
|
|
|
|
# VERSION identifies a build. It defaults to the current commit (with -dirty if
|
|
# the tree has uncommitted changes), so a stamped build is the default rather
|
|
# than something to remember. Override for a release tag: VERSION=v1.2.0.
|
|
VERSION ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)$(shell git diff --quiet 2>/dev/null || echo -dirty)
|
|
IMAGE ?= doormile/krowbackend:latest
|
|
|
|
# Exported, not just defined: docker-compose.yml reads ${VERSION} and ${IMAGE}
|
|
# from the environment, and a make variable is not in a recipe's environment
|
|
# unless it is exported. Without this the compose build arg falls back to "dev"
|
|
# and every image reports the same version.
|
|
export VERSION
|
|
export IMAGE
|
|
|
|
.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 -ldflags="-X main.version=$(VERSION)" -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: eval
|
|
eval: ## Run the agent eval suites (§9 — run this on every loop, retrieval or prompt change)
|
|
cd go-api && go test ./internal/evals/... -run "Suite|Detector" -v -count=1
|
|
|
|
.PHONY: import-agents
|
|
import-agents: ## Publish the specs in agents/ into a tenant: make import-agents ORG=<slug>
|
|
@test -n "$(ORG)" || { echo "usage: make import-agents ORG=<slug>"; exit 1; }
|
|
cd go-api && go run ./cmd/importagents --dir ../agents --skills ../skills --org "$(ORG)"
|
|
|
|
.PHONY: check-agents
|
|
check-agents: ## Parse every spec in agents/ and report, writing nothing
|
|
cd go-api && go run ./cmd/importagents --dir ../agents --skills ../skills --org check --dry-run
|
|
|
|
.PHONY: eval-live
|
|
eval-live: ## Run the eval suites against the REAL model (needs a key, costs tokens)
|
|
@test -n "$$MODEL_API_KEY" || { \
|
|
echo "eval-live needs MODEL_API_KEY — it calls a real model and costs tokens."; \
|
|
echo "The scripted suites (make eval) are the gate; this is the confirmation."; \
|
|
echo ""; \
|
|
if [ -n "$$ANTHROPIC_API_KEY" ]; then \
|
|
echo "NOTE: ANTHROPIC_API_KEY is set and is no longer read — the Anthropic"; \
|
|
echo " path was removed. Rename it to MODEL_API_KEY, and replace the"; \
|
|
echo " value if it is an Anthropic key."; \
|
|
echo ""; \
|
|
fi; \
|
|
echo "Defaults to Groq. To evaluate somewhere else, point it there:"; \
|
|
echo " MODEL_BASE_URL=https://api.groq.com/openai/v1 \\"; \
|
|
echo " MODEL_API_KEY=... MODEL_BALANCED=<model-id> make eval-live"; \
|
|
exit 1; }
|
|
cd go-api && go test ./internal/evals/ -run "TestLive" -v -count=1 -timeout 10m
|
|
|
|
.PHONY: ingest
|
|
ingest: ## Ingest knowledge/ into a tenant: make ingest ORG=<slug>
|
|
@test -n "$(ORG)" || { echo "usage: make ingest ORG=<slug>"; exit 1; }
|
|
cd go-api && go run ./cmd/ingest --dir ../knowledge --org "$(ORG)"
|
|
|
|
.PHONY: reembed
|
|
reembed: ## Re-embed a tenant's corpus with the current model: make reembed ORG=<slug>
|
|
@test -n "$(ORG)" || { echo "usage: make reembed ORG=<slug>"; exit 1; }
|
|
cd go-api && go run ./cmd/reembed --org "$(ORG)"
|
|
|
|
.PHONY: knowledge
|
|
knowledge: ## Run the retrieval layer's permission tests
|
|
cd go-api && go test ./internal/knowledge/ -v -count=1
|
|
|
|
.PHONY: tools
|
|
tools: ## Show what every agent tool returns against the seeded data
|
|
cd go-api && go test ./internal/tools/ -run TestDumpToolOutput -v
|
|
|
|
.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
|
|
|
|
# seed/fixtures/seed.json is GENERATED from krow-demo/src/api/seed.js. Edit the
|
|
# seed module, not the fixture. These two targets are the only supported way to
|
|
# change it; both need the frontend repo checked out beside this one.
|
|
.PHONY: seed-fixture
|
|
seed-fixture: ## Regenerate seed.json from the frontend seed module
|
|
cd "$(CURDIR)/../krow-demo" && npm run seed:fixture
|
|
|
|
.PHONY: seed-fixture-check
|
|
seed-fixture-check: ## Fail if seed.json no longer matches the frontend seed module
|
|
cd "$(CURDIR)/../krow-demo" && npm run seed:check
|
|
|
|
# Moving a tenant between databases. `seed` replays the frontend's demo
|
|
# fixture; this replays what is actually IN a database, so work done through
|
|
# the API comes with it. The output is real tenant data and is gitignored —
|
|
# carry it to the target over scp, never through git.
|
|
.PHONY: export-data
|
|
export-data: ## Export this database's rows as replayable SQL: make export-data [OUT=path]
|
|
python3 scripts/export-local-data.py
|
|
|
|
.PHONY: import-data
|
|
import-data: ## Apply an export to a target: make import-data TARGET_URL=postgres://… [IN=path]
|
|
@test -n "$(TARGET_URL)" || { echo "import-data: TARGET_URL is required"; exit 1; }
|
|
psql "$(TARGET_URL)" --single-transaction -v ON_ERROR_STOP=1 -f "$(or $(IN),seed/exports/local-data.sql)"
|
|
|
|
.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
|
|
|
|
# Auth runs before routing, so an unauthenticated probe answers 401 for every
|
|
# path — including ones that do not exist. Verifying a deployment therefore
|
|
# needs a session, and the credentials come from the environment so they never
|
|
# reach argv or shell history:
|
|
#
|
|
# KROW_EMAIL=... KROW_PASSWORD=... make verify-deploy BASE=https://your-host
|
|
#
|
|
# Read-only. Add ARGS=--write to exercise the write paths as well.
|
|
.PHONY: verify-deploy
|
|
verify-deploy: ## Check every endpoint on a deployment: make verify-deploy BASE=<url>
|
|
@python3 scripts/verify-deploy.py "$(or $(BASE),http://127.0.0.1:8080)" $(ARGS)
|
|
|
|
.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
|