Skip to content

Commit 5590e77

Browse files
authored
Merge pull request #747 from Nickatak/chore/makefile-rewrite
chore: Adopt Make as canonical task runner with working host + stage workflows
2 parents 2dd5b09 + 49721e9 commit 5590e77

9 files changed

Lines changed: 576 additions & 58 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
default_language_version:
1313
python: python3.13
14-
node: "24"
14+
node: system
1515

1616
repos:
1717
# File-shape hygiene. These are pre-commit's own hooks, applied to every

Makefile

Lines changed: 289 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,301 @@
1-
.PHONY: build start stop build-linter lint migrations migrate db-shell test-server test-frontend
1+
.PHONY: help \
2+
local-check-prereqs \
3+
local-install local-install-frontend local-install-backend \
4+
local-run-frontend local-run-backend local-check-db \
5+
local-stop-docker-frontend local-stop-docker-backend \
6+
local-makemigrations local-superuser \
7+
local-test local-test-backend local-test-frontend \
8+
local-clean local-kill-ports \
9+
docker-up db-up docker-down docker-logs docker-watch \
10+
db-migrate db-makemigrations db-reset db-reset-hard db-grant-test-db-perms \
11+
stage-smoke stage-up stage-down \
12+
docker-shell-backend docker-shell-frontend docker-shell-db \
13+
lint install-hooks
214

3-
# Run commands with `make <target>`
15+
BACKEND_DIR := backend
16+
FRONTEND_DIR := frontend
17+
DEV_ENV := dev/dev.env
18+
# Source dev env vars before invoking poetry so host Django targets get the same
19+
# config docker-compose loads via env_file: dev/dev.env. SQL_HOST is overridden
20+
# because dev.env's `pgdb` is a docker-network DNS name; on host, pgdb's port
21+
# is published on localhost.
22+
BACKEND_RUN := set -a; . $(DEV_ENV); SQL_HOST=localhost; set +a; cd $(BACKEND_DIR) && poetry run
23+
DEV_COMPOSE ?= docker compose
24+
STAGE_COMPOSE ?= docker compose -f docker-compose.stage.yml
25+
STAGE_ENV := stage/stage.env
26+
DB_SERVICE ?= pgdb
27+
BACKEND_SERVICE ?= django
28+
FRONTEND_SERVICE ?= next
29+
LOCAL_KILL_PORTS ?= 3000 8000 5432
430

5-
build:
6-
docker compose build
31+
# ============================================================================
32+
# HELP
33+
# ============================================================================
734

8-
start:
9-
docker compose up
35+
help:
36+
@echo "civictechjobs - command reference"
37+
@echo ""
38+
@echo "Core Local Workflow (host processes; rapid iteration)"
39+
@echo " make local-check-prereqs Verify host has node 24 / python 3.13 / poetry / docker"
40+
@echo " make local-install Install frontend + backend dependencies"
41+
@echo " make local-run-frontend Stop docker frontend, run Next.js on host"
42+
@echo " make local-run-backend Stop docker backend, run Django on host"
43+
@echo " make local-test Run backend tests + frontend tests"
44+
@echo " make local-makemigrations Create Django migration files (host)"
45+
@echo " make local-superuser Create Django admin user (host)"
46+
@echo ""
47+
@echo "Core Dev Docker Workflow"
48+
@echo " make docker-up Start full dev stack (detached, with build)"
49+
@echo " make db-up Start only the postgres service"
50+
@echo " make docker-down Stop dev stack"
51+
@echo " make docker-logs Stream dev stack logs"
52+
@echo " make docker-watch Run compose watch (live host->container sync)"
53+
@echo " make db-migrate Apply Django migrations against dev DB"
54+
@echo " make db-makemigrations Create Django migration files (in container)"
55+
@echo " make db-reset Truncate all app data (keeps schema + migrations)"
56+
@echo " make db-reset-hard Drop dev DB volume and recreate DB container"
57+
@echo " make db-grant-test-db-perms Grant test-DB CREATEDB perms (for Django tests)"
58+
@echo ""
59+
@echo "Stage smoke test (local simulation of production-shaped builds; deploy lives in incubator)"
60+
@echo " make stage-smoke PRIMARY: build + up + probe healthchecks + tear down (one-shot pass/fail)"
61+
@echo " make stage-up Bring stage stack up for debugging (only when stage-smoke fails)"
62+
@echo " make stage-down Tear stage stack down"
63+
@echo ""
64+
@echo "Lint and hooks"
65+
@echo " make lint Run pre-commit across all files (full suite)"
66+
@echo " make install-hooks Install pre-commit git hook"
67+
@echo ""
68+
@echo "Shell Access"
69+
@echo " make docker-shell-backend Shell into dev backend container"
70+
@echo " make docker-shell-frontend Shell into dev frontend container"
71+
@echo " make docker-shell-db psql shell into dev DB container"
72+
@echo ""
73+
@echo "Local Utilities"
74+
@echo " make local-clean Clear local build/cache artifacts"
75+
@echo " make local-kill-ports Manual rescue for ports $(LOCAL_KILL_PORTS)"
1076

11-
stop:
12-
docker compose down
77+
# ============================================================================
78+
# LOCAL (HOST PROCESSES)
79+
# ============================================================================
1380

14-
build-linter:
15-
docker compose build linter
81+
local-check-prereqs:
82+
@status=0; \
83+
echo "Checking host prerequisites:"; \
84+
echo ""; \
85+
if command -v node >/dev/null 2>&1; then \
86+
v=$$(node --version | sed 's/v//'); maj=$${v%%.*}; \
87+
if [ "$$maj" -ge 24 ]; then echo " [OK] node $$v"; \
88+
else echo " [OUTDATED] node $$v (need >=24)"; status=1; fi; \
89+
else echo " [MISSING] node (need >=24)"; status=1; fi; \
90+
if command -v python3.13 >/dev/null 2>&1; then \
91+
echo " [OK] python3.13 $$(python3.13 --version | awk '{print $$2}')"; \
92+
else echo " [MISSING] python3.13 (poetry / Django require ^3.13)"; status=1; fi; \
93+
if command -v poetry >/dev/null 2>&1; then \
94+
echo " [OK] poetry $$(poetry --version 2>&1 | awk '{print $$3}' | tr -d ')')"; \
95+
else echo " [MISSING] poetry (https://python-poetry.org/docs/#installation)"; status=1; fi; \
96+
if command -v docker >/dev/null 2>&1; then \
97+
echo " [OK] docker $$(docker --version | awk '{print $$3}' | tr -d ',')"; \
98+
else echo " [MISSING] docker (required for db-up and full dev stack)"; status=1; fi; \
99+
echo ""; \
100+
if [ "$$status" -eq 0 ]; then echo "All prerequisites satisfied."; \
101+
else echo "Missing or outdated prerequisites - see docs/developer/installation.md"; exit 1; fi
102+
103+
local-install: local-install-frontend local-install-backend
104+
105+
local-install-frontend:
106+
rm -rf $(FRONTEND_DIR)/node_modules
107+
npm ci --prefix $(FRONTEND_DIR)
108+
109+
local-install-backend:
110+
rm -rf $(BACKEND_DIR)/.venv
111+
cd $(BACKEND_DIR) && python3.13 -m venv .venv && poetry install
112+
113+
local-stop-docker-frontend:
114+
@$(DEV_COMPOSE) stop $(FRONTEND_SERVICE) >/dev/null 2>&1 || true
115+
116+
local-stop-docker-backend:
117+
@$(DEV_COMPOSE) stop $(BACKEND_SERVICE) >/dev/null 2>&1 || true
118+
119+
local-run-frontend: local-stop-docker-frontend
120+
cd $(FRONTEND_DIR) && npm run dev
121+
122+
local-run-backend: local-stop-docker-backend local-check-db
123+
$(BACKEND_RUN) python manage.py runserver
124+
125+
local-check-db:
126+
@nc -z localhost 5432 2>/dev/null || (echo "DB not reachable on :5432. Run 'make db-up' first."; exit 1)
127+
128+
local-makemigrations:
129+
$(BACKEND_RUN) python manage.py makemigrations
130+
131+
local-superuser:
132+
$(BACKEND_RUN) python manage.py createsuperuser
133+
134+
local-test: local-test-backend local-test-frontend
135+
136+
local-test-backend:
137+
$(BACKEND_RUN) python manage.py test ctj_api.tests --keepdb --noinput
138+
139+
local-test-frontend:
140+
cd $(FRONTEND_DIR) && npx vitest run
141+
142+
local-clean:
143+
rm -rf $(FRONTEND_DIR)/.next $(FRONTEND_DIR)/node_modules
144+
find . -type d -name "__pycache__" -exec rm -rf {} +
145+
find . -type f -name "*.pyc" -delete
146+
@echo "Clean complete."
147+
148+
local-kill-ports:
149+
@echo "Stopping listeners on ports: $(LOCAL_KILL_PORTS)"
150+
@for port in $(LOCAL_KILL_PORTS); do \
151+
if command -v fuser >/dev/null 2>&1; then \
152+
echo "Port $$port: sending TERM"; \
153+
fuser -k -TERM $$port/tcp 2>/dev/null || true; \
154+
sleep 1; \
155+
if fuser $$port/tcp >/dev/null 2>&1; then \
156+
echo "Port $$port: still busy, sending KILL"; \
157+
fuser -k -KILL $$port/tcp 2>/dev/null || true; \
158+
fi; \
159+
elif command -v lsof >/dev/null 2>&1; then \
160+
pids=$$(lsof -tiTCP:$$port -sTCP:LISTEN 2>/dev/null || true); \
161+
if [ -n "$$pids" ]; then \
162+
echo "Port $$port: sending TERM to PID(s) $$pids"; \
163+
kill -TERM $$pids 2>/dev/null || true; \
164+
sleep 1; \
165+
pids=$$(lsof -tiTCP:$$port -sTCP:LISTEN 2>/dev/null || true); \
166+
if [ -n "$$pids" ]; then \
167+
echo "Port $$port: still busy, sending KILL to PID(s) $$pids"; \
168+
kill -KILL $$pids 2>/dev/null || true; \
169+
fi; \
170+
else \
171+
echo "Port $$port: no listener"; \
172+
fi; \
173+
else \
174+
echo "Port $$port: skipped (install fuser or lsof)"; \
175+
fi; \
176+
done
177+
178+
# ============================================================================
179+
# DOCKER DEV
180+
# ============================================================================
181+
182+
docker-up:
183+
$(DEV_COMPOSE) up -d --build
184+
185+
db-up:
186+
$(DEV_COMPOSE) up -d $(DB_SERVICE)
187+
188+
docker-down:
189+
$(DEV_COMPOSE) down --remove-orphans
190+
191+
docker-logs:
192+
$(DEV_COMPOSE) logs -f --tail=200
193+
194+
docker-watch:
195+
$(DEV_COMPOSE) watch
196+
197+
db-migrate:
198+
$(DEV_COMPOSE) exec $(BACKEND_SERVICE) python manage.py migrate
199+
200+
db-makemigrations:
201+
$(DEV_COMPOSE) exec $(BACKEND_SERVICE) python manage.py makemigrations
202+
203+
db-reset:
204+
@echo "This will truncate all app data (schema + migrations preserved). Type 'yes' to confirm:"
205+
@read ans && [ "$$ans" = "yes" ] || (echo "Aborted."; exit 1)
206+
$(DEV_COMPOSE) exec $(BACKEND_SERVICE) python manage.py flush --noinput
207+
208+
db-reset-hard:
209+
@echo "This will destroy the DB volume and all data. Type 'yes' to confirm:"
210+
@read ans && [ "$$ans" = "yes" ] || (echo "Aborted."; exit 1)
211+
$(DEV_COMPOSE) down -v --remove-orphans
212+
$(DEV_COMPOSE) up -d $(DB_SERVICE)
213+
214+
db-grant-test-db-perms:
215+
$(DEV_COMPOSE) exec -T $(DB_SERVICE) psql -U $${POSTGRES_USER:-postgres} -d $${POSTGRES_DB:-postgres} -c "ALTER USER $${POSTGRES_USER:-postgres} CREATEDB;"
216+
217+
# ============================================================================
218+
# STAGE (LOCAL SIMULATION OF THE STAGE-DEPLOYED SHAPE)
219+
# ============================================================================
220+
# Real stage deployment lives in HfLA's incubator repo (Terraform). These
221+
# targets exercise the production-shaped images and compose locally so we
222+
# can catch image / config / migration issues before pushing.
223+
#
224+
# stage-smoke is the primary tool: builds, ups, probes, tears down, exits
225+
# 0 on green. stage-up / stage-down exist for debugging when smoke fails.
226+
227+
stage-smoke:
228+
@if [ ! -f $(STAGE_ENV) ]; then \
229+
echo "ERROR: $(STAGE_ENV) does not exist."; \
230+
echo "Copy stage/stage.env.example to $(STAGE_ENV) and fill in the placeholders before running stage-smoke."; \
231+
exit 1; \
232+
fi
233+
@echo "[stage-smoke] building images..."
234+
$(STAGE_COMPOSE) build
235+
@echo "[stage-smoke] starting stack..."
236+
$(STAGE_COMPOSE) up -d
237+
@echo "[stage-smoke] probing django (/api/healthcheck/)..."
238+
@for i in $$(seq 1 60); do \
239+
if curl -fsS http://localhost:8000/api/healthcheck/ >/dev/null 2>&1; then \
240+
echo " django OK"; break; \
241+
fi; \
242+
if [ $$i -eq 60 ]; then \
243+
echo " django did not respond after 120s"; \
244+
$(STAGE_COMPOSE) logs --tail=80; \
245+
$(STAGE_COMPOSE) down --remove-orphans; \
246+
exit 1; \
247+
fi; \
248+
sleep 2; \
249+
done
250+
@echo "[stage-smoke] probing next (/)..."
251+
@for i in $$(seq 1 60); do \
252+
if curl -fsS http://localhost:3000/ >/dev/null 2>&1; then \
253+
echo " next OK"; break; \
254+
fi; \
255+
if [ $$i -eq 60 ]; then \
256+
echo " next did not respond after 120s"; \
257+
$(STAGE_COMPOSE) logs --tail=80; \
258+
$(STAGE_COMPOSE) down --remove-orphans; \
259+
exit 1; \
260+
fi; \
261+
sleep 2; \
262+
done
263+
@echo "[stage-smoke] all probes passed; tearing down..."
264+
$(STAGE_COMPOSE) down --remove-orphans
265+
@echo "[stage-smoke] OK"
266+
267+
stage-up:
268+
@if [ ! -f $(STAGE_ENV) ]; then \
269+
echo "ERROR: $(STAGE_ENV) does not exist."; \
270+
echo "Copy stage/stage.env.example to $(STAGE_ENV) and fill in the placeholders."; \
271+
exit 1; \
272+
fi
273+
$(STAGE_COMPOSE) up -d --build
274+
275+
stage-down:
276+
$(STAGE_COMPOSE) down --remove-orphans
277+
278+
# ============================================================================
279+
# LINT AND HOOKS
280+
# ============================================================================
16281

17282
lint:
18-
docker compose up linter
283+
$(BACKEND_DIR)/.venv/bin/pre-commit run --all-files
284+
285+
install-hooks:
286+
$(BACKEND_DIR)/.venv/bin/pre-commit install
19287

20-
# Below commands require the docker containers to be running
288+
# ============================================================================
289+
# SHELL ACCESS
290+
# ============================================================================
21291

22-
migrations:
23-
docker compose exec django python manage.py makemigrations
292+
docker-shell-backend:
293+
$(DEV_COMPOSE) exec $(BACKEND_SERVICE) sh
24294

25-
migrate:
26-
docker compose exec django python manage.py migrate
295+
docker-shell-frontend:
296+
$(DEV_COMPOSE) exec $(FRONTEND_SERVICE) sh
27297

28-
db-shell:
29-
docker compose exec django python manage.py shell
298+
docker-shell-db:
299+
$(DEV_COMPOSE) exec $(DB_SERVICE) psql -U $${POSTGRES_USER:-postgres} -d $${POSTGRES_DB:-postgres}
30300

31-
test-server:
32-
docker exec django python manage.py test server.tests
301+
.DEFAULT_GOAL := help

backend/.python-version

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)