Files
krow_backend/Makefile
Suriyakumarvijayanayagam 954ba9076f Add CORS credentials, transactional endpoints, and container deployment
CORS
  cors.go never set Access-Control-Allow-Credentials, so the
  cookie-authenticated API was unreadable from any cross-origin frontend:
  the server answered correctly and the browser blocked the page from
  reading it. Set for allowlisted origins on both the preflight and the
  actual response. Three tests added.

  HTTP_COOKIE_SAMESITE (lax|none|strict, default lax) is new. CORS is only
  half of what a cross-origin browser call needs; SameSite is judged on
  registrable domain, so a frontend on an unrelated domain gets perfect CORS
  headers and still no cookie. "none" is the only value that survives that,
  and validate() refuses it without the Secure flag.

  The "*" rejection now explains itself: browsers refuse Allow-Origin "*"
  together with credentials, so it would break every authenticated call
  rather than loosen anything.

Transactional endpoints (api-contract.md 12.1)
  POST /api/v1/job-applications/{id}/hire
  POST /api/v1/job-postings/{id}/assignments

  Replaces two client-side loops that wrote several records with no
  transaction and no rollback. Each is now one endpoint and one transaction,
  built over repo.Repo so org scoping, derived columns, type casts and error
  translation are not re-derived. Authorization reuses the existing policy
  table rather than adding a parallel one: a workflow is exactly as
  privileged as the writes it performs. 13 tests, including both rollback
  paths.

Bug fix in the repository layer
  repo.bindValue handled int64/int/float64/string but not int32, which is
  what pgx returns for a PostgreSQL `int` column. Nothing previously read a
  record and wrote one of its fields elsewhere, so it never surfaced; the
  hire flow does exactly that and failed with "ai_score must be a number".
  Both KindInt and KindFloat now accept the widths pgx actually produces.

Deployment
  infrastructure/Dockerfile.api  multi-stage, cross-compiling (BUILDPLATFORM
    + GOARCH) so linux/amd64 builds from arm64 are compiled rather than
    emulated. Alpine runtime, non-root uid 10001, 22.1 MB. Ships api, seed,
    setpassword and migrate, plus the migrations, so a Kubernetes
    initContainer can apply the schema from the same image and tag as the
    API. HEALTHCHECK keys on status code, not body, so a "degraded" instance
    is not pulled from rotation during a migration window.

  infrastructure/docker-compose.yml  migrations run to completion before the
    API starts. Assumes a managed PostgreSQL; the local-db overlay adds one
    with TLS enabled so APP_ENV=production is met rather than dodged.

  scripts/drop_public_tables.go  the one-off used to clear an unrelated
    schema from krowdb on 2026-08-24, kept for the record. Build-tagged
    ignore and gated on CONFIRM_DROP=yes.

Verified against PostgreSQL: 16/16 new tests pass, and the image was built,
run and exercised end to end (login, CORS preflight, authenticated reads,
transaction rollback).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmQiGq73Uyfq7J4yR8Vxxw
2026-08-25 11:33:01 +05:30

192 lines
7.5 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)"
.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