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
17282lint :
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
0 commit comments