Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
867 lines (754 loc) Β· 42.7 KB
/
Copy pathMakefile
File metadata and controls
867 lines (754 loc) Β· 42.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
SHELL := /bin/bash
# Every target is phony (none produces a file of its own name). Keep this list in
# definition order, one line per "##@" section, so it doubles as a table of contents
# and any drift from the file layout is visible in review.
.PHONY: .build .clean-stale \
postgres-up postgres-down postgres-login postgres-status postgres-status-full \
.drop-database .migrate-database .load-test-data postgres-drop postgres-migrate postgres-load postgres-drop-migrate postgres-drop-migrate-load postgres-size postgres-count postgres-auditlog postgres-clean-testrun postgres-prep-area-sql \
dbgate-up dbgate-down dbgate-restart dbgate-status \
keycloak-up keycloak-down .keycloak-wait .keycloak-realm .keycloak-admin .keycloak-roles keycloak-generate-machine-clients keycloak-configure keycloak-show-client-public-key keycloak-match-client-public-keys .get-client-credentials \
backend-up backend-down backend-restart \
up down restart status \
.is-up .ensure-up test-smoke test-full test-full-keep test-full-verbose test-suites test-ca test-str test-sta test-lsa test-lma test-ama test-security \
.postgres-up-unless-ci test-migrations \
test-perf test-perf-keep test-perf-verbose \
test-malware test-cve test-cve-offline \
test test-keep \
md-lint md-format md-validate-links \
spell-check \
all ci-gate dod dod-continue \
postgres-logs keycloak-logs backend-logs dbgate-logs fullstack-logs \
help
.DEFAULT_GOAL := help
ifndef CI
-include .env
-include .env.extra
endif
POSTGRES_HOST ?= localhost
export POSTGRES_HOST POSTGRES_PORT POSTGRES_DB_NAME POSTGRES_DB_USER POSTGRES_DB_PASSWORD
DOCKER_COMPOSE := docker compose --env-file .env $(if $(wildcard .env.extra),--env-file .env.extra,)
# Client-signed JWT test clients. The generator emits one client per role (CA,
# STR, STA, LSA, LMA, AMA) into KEYCLOAK_JWT_CLIENT_DIR, named
# "<client-id>.private.pem" and "<client-id>.public.yaml", so any client's key
# path follows from its id and needs no variable of its own. Keep MACHINE_CLIENTS_EXTENDED_YAML in sync with
# KC_APP_REALM_MACHINE_CLIENT_YAML in .env - both name the same file.
KEYCLOAK_JWT_CLIENT_DIR ?= tmp
MACHINE_CLIENTS_EXTENDED_YAML ?= tmp/machine-clients-extended.yaml
# Disable BuildKit provenance/SBOM attestations. With the containerd image store,
# attestations wrap the image in a manifest list and embed build timestamps, so
# every build β even a fully cached one β yields a new image digest. That makes
# `docker compose up -d` needlessly recreate the backend container on each `make up`.
export BUILDX_NO_DEFAULT_ATTESTATIONS := 1
DBGATE_PID_FILE := /tmp/dbgate.pid
DBGATE_PROCESS_PATTERN := /tmp/.mount_[d]bgate.*/dbgate|dbgate-7\.1\.2-linux_x86_64\.AppImage
# Progress messages follow the target they belong to: the opening line restates the
# target's "##" help text as "<verb>ing ...", the closing line as "β
<verb>ed ...!".
# Emoji carry the action, so a scrolling log stays scannable:
# π start π stop ποΈ drop π change/configure π₯ load π³ build image
# π show π§ͺ test π scan π format π logs π login
# β
done β failed β οΈ warning βΉοΈ hint
# Common helpers
.build: ## Build
@echo "π³ Building fullstack..."
$(DOCKER_COMPOSE) build
@echo "β
Fullstack built!"
@echo "π Images:"
@set -a && source .env && set +a && docker images | grep $$APP_PREFIX
.clean-stale: ## Remove stale containers
@echo "π§Ή Cleaning stale containers..."
@set -a && source .env && set +a && \
docker ps -a --filter "name=$$APP_PREFIX" --filter "status=exited" -q | xargs -r docker rm -f || true
@$(DOCKER_COMPOSE) rm -f initdb 2>/dev/null || true
@echo "β
Stale containers cleaned!"
##@ Postgres
postgres-up: .clean-stale ## Start postgres
@echo "π Starting postgres..."
$(DOCKER_COMPOSE) up -d --wait postgres
@echo "β
Postgres started!"
postgres-down: ## Stop and remove postgres (including volumes)
@echo "π Stopping and removing postgres (including volumes)..."
$(DOCKER_COMPOSE) stop postgres
$(DOCKER_COMPOSE) rm -f -v postgres
@docker volume rm $$(docker volume ls -q | grep postgres_data) 2>/dev/null || true
@echo "β
Postgres stopped, removed, and volumes cleaned!"
postgres-login: ## Login to postgres
@echo "π Logging in to postgres..."
docker exec -it $$($(DOCKER_COMPOSE) ps -q postgres) psql -U postgres -d sdep-data
postgres-status: ## Show postgres tables
@set -a && source .env && set +a && \
echo "π Showing tables for database $$POSTGRES_DB_NAME..." && \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -c "\\dt"
@echo ""
@echo "π Showing structure of each table..."
@set -a && source .env && set +a && \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -t -c "SELECT tablename FROM pg_tables WHERE schemaname='public'" | \
while read -r table; do \
if [ -n "$$table" ]; then \
echo ""; \
echo "=== Table: $$table ==="; \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -c "SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name='$$table' ORDER BY ordinal_position"; \
fi; \
done
postgres-status-full: postgres-status ## Show postgres tables with full details
@echo ""
@echo "π Showing full structure of each table..."
@set -a && source .env && set +a && \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -t -c "SELECT tablename FROM pg_tables WHERE schemaname='public'" | \
while read -r table; do \
if [ -n "$$table" ]; then \
echo ""; \
echo "=== Table: $$table (full details) ==="; \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -c "\\d+ $$table"; \
fi; \
done
##@ Postgres (data)
.drop-database: ## Drop and recreate database (empty)
@set -a && source .env && set +a && \
echo "ποΈ Dropping and recreating database $$POSTGRES_DB_NAME..." && \
docker exec -i sdep-postgres psql -U $$POSTGRES_SUPER_USER -d postgres < postgres/clean-app.sql
@echo "β
Database dropped and recreated!"
.migrate-database: ## Migrate database (create/update tables)
@echo "π Running database migrations..."
@docker exec -i $$($(DOCKER_COMPOSE) ps -q backend) alembic upgrade head
@echo "β
Database migrations completed!"
.load-test-data: ## Load test data into database
@echo "π₯ Loading test data..."
@set -a && source .env && set +a && \
echo "Using PostgreSQL user: $$POSTGRES_SUPER_USER" && \
echo "Connecting to database: $$POSTGRES_DB_NAME" && \
echo "Executing SQL files..." && \
for sql_file in $$(ls test-data/*.sql 2>/dev/null | sort); do \
echo " Executing: $$sql_file"; \
docker exec -i sdep-postgres psql -U $$POSTGRES_SUPER_USER -d $$POSTGRES_DB_NAME -v ON_ERROR_STOP=1 < "$$sql_file"; \
done
@echo "β
Test data loaded!"
postgres-drop: .clean-stale ## Drop tables (recreate empty database)
@echo "ποΈ Dropping sdep-database tables..."
@$(MAKE) --no-print-directory .drop-database
@echo "β
SDEP database tables dropped!"
postgres-migrate: ## Migrate tables (create/update)
@echo "π Migrating sdep-database..."
@$(MAKE) --no-print-directory .migrate-database
@echo "β
SDEP database migrated!"
postgres-load: .clean-stale ## Load test data (01-competent-authority.sql + 02-area-generated.sql)
@echo "π₯ Loading test data into sdep-database..."
@$(MAKE) --no-print-directory .load-test-data
@echo "β
SDEP test data loaded!"
postgres-drop-migrate: .clean-stale ## Drop + migrate
@echo "π Dropping and migrating sdep-database..."
@$(MAKE) --no-print-directory .drop-database .migrate-database
@echo "β
SDEP database dropped and migrated!"
postgres-drop-migrate-load: .clean-stale ## Drop + migrate + load
@echo "π Dropping, migrating and loading sdep-database..."
@$(MAKE) --no-print-directory .drop-database .migrate-database .load-test-data
@echo "β
SDEP database dropped, migrated and loaded!"
postgres-size: ## Show database size
@set -a && source .env && set +a && \
echo "π Showing size of database $$POSTGRES_DB_NAME..." && \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -c "SELECT pg_size_pretty(pg_database_size(current_database()));"
postgres-count: ## Count rows in all tables
@echo "π Counting rows in all tables..."
@set -a && source .env && set +a && \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -c " \
SELECT \
(SELECT COUNT(*) FROM activity) AS activity, \
(SELECT COUNT(*) FROM area) AS area, \
(SELECT COUNT(*) FROM audit_log) AS audit_log, \
(SELECT COUNT(*) FROM competent_authority) AS competent_authority, \
(SELECT COUNT(*) FROM listing) AS listing, \
(SELECT COUNT(*) FROM platform) AS platform;"
postgres-auditlog: ## Show audit log
@set -a && source .env && set +a && \
echo "π Showing audit log for database $$POSTGRES_DB_NAME..." && \
docker exec sdep-postgres psql -U $$POSTGRES_DB_USER -d $$POSTGRES_DB_NAME -c "SELECT * FROM audit_log"
# Recovery target: a test run cleans up after itself, so this is for the exceptions -
# inspecting data kept by a "keep" run and wanting it gone now, or a run that was
# aborted (Ctrl-C, timeout) before its cleanup. Removes sdep-test-* rows only, so
# predefined test data survives; postgres-drop is the blunt alternative.
postgres-clean-testrun: ## Clean test-run data (sdep-test-* rows only; keeps predefined test data)
@echo "π§Ή Cleaning test-run data..."
@set -a && source .env && set +a && \
docker exec -i sdep-postgres psql -U $$POSTGRES_SUPER_USER -d $$POSTGRES_DB_NAME \
-v ON_ERROR_STOP=1 < postgres/clean-testrun.sql
@echo "β
Test-run data cleaned!"
postgres-prep-area-sql: ## Generate static test-data (02-area-generated.sql, only invoke when shapefiles changed)
@echo "π Generating area SQL file with embedded shapefile data..."
@./test-data/postgres-prep-area-sql.sh
@echo "β
Area SQL file generated!"
##@ DBGate (optional)
dbgate-up: ## Start dbgate
@DBGATE_PIDS=$$(pgrep -f "$(DBGATE_PROCESS_PATTERN)" || true); \
if [ -n "$$DBGATE_PIDS" ]; then \
echo "β οΈ DBGate is already running (PID(s): $$DBGATE_PIDS)."; \
echo " Use: make dbgate-status"; \
echo " Use: make dbgate-restart"; \
exit 1; \
fi
@set -a && source .env && set +a && \
POSTGRES_STATUS=$$(docker inspect --format='{{.State.Health.Status}}' $$POSTGRES_CONTAINER_NAME 2>&1 | grep -v "^Error" || echo "not-running"); \
if [ "$$POSTGRES_STATUS" != "healthy" ]; then \
echo "β οΈ PostgreSQL container '$$POSTGRES_CONTAINER_NAME' is '$$POSTGRES_STATUS' (expected healthy)."; \
echo " Start it with: make postgres-up"; \
fi; \
echo "π Starting DBGate..." && \
echo "Use these PostgreSQL connections:" && \
echo "" && \
echo "SDEP app database:" && \
printf " %-12s %s\n" "Host:" "localhost" && \
printf " %-12s %s\n" "Port:" "$$POSTGRES_PORT" && \
printf " %-12s %s\n" "Database:" "$$POSTGRES_DB_NAME" && \
printf " %-12s %s\n" "User:" "$$POSTGRES_DB_USER" && \
printf " %-12s %s\n" "Password:" "$$POSTGRES_DB_PASSWORD" && \
printf " %-12s %s\n" "URL (opt):" "postgresql://$$POSTGRES_DB_USER:$$POSTGRES_DB_PASSWORD@localhost:$$POSTGRES_PORT/$$POSTGRES_DB_NAME" && \
echo "" && \
echo "Keycloak database:" && \
printf " %-12s %s\n" "Host:" "localhost" && \
printf " %-12s %s\n" "Port:" "$$POSTGRES_PORT" && \
printf " %-12s %s\n" "Database:" "keycloak" && \
printf " %-12s %s\n" "User:" "$$KC_DB_USERNAME" && \
printf " %-12s %s\n" "Password:" "$$KC_DB_PASSWORD" && \
printf " %-12s %s\n" "URL (opt):" "postgresql://$$KC_DB_USERNAME:$$KC_DB_PASSWORD@localhost:$$POSTGRES_PORT/keycloak" && \
echo "" && \
echo "Tip: save 2 DBGate connections (local-sdep + local-keycloak)." && \
echo "Then click the target DB node (sdep-data or keycloak) and Refresh."
@set -a && source .env && set +a && nohup dbgate "postgresql://$$POSTGRES_DB_USER:$$POSTGRES_DB_PASSWORD@localhost:$$POSTGRES_PORT/$$POSTGRES_DB_NAME" >/tmp/dbgate.log 2>&1 & echo $$! > "$(DBGATE_PID_FILE)"
@echo "β
DBGate started in background (logs: /tmp/dbgate.log)"
@echo "π DBGate web UI: http://localhost:3000"
dbgate-down: ## Stop dbgate
@PIDS=""; \
if [ -f "$(DBGATE_PID_FILE)" ]; then \
PID_FROM_FILE=$$(cat "$(DBGATE_PID_FILE)" 2>/dev/null || true); \
if [ -n "$$PID_FROM_FILE" ] && kill -0 "$$PID_FROM_FILE" 2>/dev/null; then \
PIDS="$$PID_FROM_FILE"; \
fi; \
fi; \
if [ -z "$$PIDS" ]; then \
PIDS=$$(pgrep -f "$(DBGATE_PROCESS_PATTERN)" || true); \
fi; \
if [ -n "$$PIDS" ]; then \
echo "π Stopping DBGate..."; \
kill $$PIDS; \
rm -f "$(DBGATE_PID_FILE)"; \
echo "β
DBGate stopped!"; \
else \
rm -f "$(DBGATE_PID_FILE)"; \
echo "βΉοΈ DBGate is not running"; \
fi
dbgate-restart: dbgate-down dbgate-up ## Restart dbgate
dbgate-status: ## Show dbgate status and database connection details
@set -a && source .env && set +a && \
POSTGRES_STATUS=$$(docker inspect --format='{{.State.Health.Status}}' $$POSTGRES_CONTAINER_NAME 2>&1 | grep -v "^Error" || echo "not-running"); \
DBGATE_PS=$$(pgrep -af "$(DBGATE_PROCESS_PATTERN)" || true); \
PID_FILE_INFO="missing"; \
if [ -f "$(DBGATE_PID_FILE)" ]; then PID_FILE_INFO=$$(cat "$(DBGATE_PID_FILE)" 2>/dev/null || echo "invalid"); fi; \
echo "π Postgres status"; \
printf " %-12s %s\n" "Postgres:" "$$POSTGRES_STATUS"; \
echo ""; \
echo "π DBGate status (optional)"; \
printf " %-16s %s\n" "DBGate pid file:" "$$PID_FILE_INFO"; \
if [ -n "$$DBGATE_PS" ]; then \
printf " %-16s %s\n" "DBGate:" "running"; \
echo " Processes:"; \
echo "$$DBGATE_PS"; \
else \
printf " %-16s %s\n" "DBGate:" "stopped"; \
fi; \
printf " %-16s %s\n" "DBGate UI:" "http://localhost:3000"; \
printf " %-16s %s\n" "Postgres SDEP:" "postgresql://$$POSTGRES_DB_USER:$$POSTGRES_DB_PASSWORD@localhost:$$POSTGRES_PORT/$$POSTGRES_DB_NAME"; \
printf " %-16s %s\n" "Postgres KC:" "postgresql://$$KC_DB_USERNAME:$$KC_DB_PASSWORD@localhost:$$POSTGRES_PORT/keycloak"
##@ Keycloak
keycloak-up: postgres-up ## Start keycloak + generate machine clients + configure
@echo "π Starting Keycloak..."
# --build: rebuild when keycloak/Dockerfile changed; a cache hit otherwise (<1s)
$(DOCKER_COMPOSE) up -d --build keycloak
@echo "β
Keycloak started!"
@echo "π Configuring Keycloak..."
@$(MAKE) --no-print-directory keycloak-configure
@echo "β
Keycloak configured!"
keycloak-down: ## Stop and remove keycloak (including volumes)
@echo "π Stopping and removing Keycloak (including volumes)..."
$(DOCKER_COMPOSE) stop keycloak
$(DOCKER_COMPOSE) rm -f -v keycloak
@echo "β
Keycloak stopped, removed, and volumes cleaned!"
.keycloak-wait: ## Wait until keycloak allows to authenticate
@./keycloak/wait.sh
@set -a && source .env && set +a && echo "β
Keycloak ready at $$KC_BASE_URL"
.keycloak-realm: .keycloak-wait ## Create realm
@set -a && source .env && set +a && ./keycloak/add-realm.sh
.keycloak-admin: .keycloak-realm ## Create (CI/CD) admin account in realm
@mkdir -p ./tmp
@set -a && source .env && set +a && \
KC_APP_REALM_ADMIN_SECRET=$$(bash keycloak/add-realm-admin.sh | grep "Client Secret:" | cut -d' ' -f3) && \
echo "$$KC_APP_REALM_ADMIN_SECRET" > ./tmp/KC_APP_REALM_ADMIN_SECRET.txt
.keycloak-roles: .keycloak-admin ## Create roles in realm (keycloak/roles.yaml)
@set -a && source .env && set +a && \
export KC_APP_REALM_ADMIN_SECRET=$$(cat ./tmp/KC_APP_REALM_ADMIN_SECRET.txt) && \
./keycloak/add-realm-roles.sh
keycloak-generate-machine-clients: ## Generate machine clients (from keycloak/machine-clients.yaml, adding client-signed JWT key pairs for CA, STR, STA, LSA, LMA, AMA)
@uv run --script scripts/generate-keycloak-machine-clients.py \
--output-dir "$(KEYCLOAK_JWT_CLIENT_DIR)" \
--static-clients-file keycloak/machine-clients.yaml \
--extended-clients-file "$(MACHINE_CLIENTS_EXTENDED_YAML)"
keycloak-configure: .keycloak-roles keycloak-generate-machine-clients ## Configure keycloak (realm, roles, machine clients)
@set -a && source .env && set +a && \
export KC_APP_REALM_ADMIN_SECRET=$$(cat ./tmp/KC_APP_REALM_ADMIN_SECRET.txt) && \
export KC_APP_REALM_MACHINE_CLIENT_YAML="$(MACHINE_CLIENTS_EXTENDED_YAML)" && \
echo "Machine client configuration: $$KC_APP_REALM_MACHINE_CLIENT_YAML" && \
./keycloak/add-realm-machine-clients.sh
# CLIENT_ID has no default on purpose: the generator creates one client per role,
# so the client to inspect is always an explicit choice. KC_ENV defaults to
# "local" via .env; re-exporting it after sourcing .env lets an explicit
# "make <target> KC_ENV=tst" win over that default.
keycloak-show-client-public-key: ## Show client-signed JWT public key (retrieve from keycloak)
@if [ -z "$(CLIENT_ID)" ]; then \
echo "β Error: CLIENT_ID is not set"; \
echo " Example: make keycloak-show-client-public-key CLIENT_ID=sdep-test-str.jwt"; \
exit 1; \
fi
@set -a && source .env && set +a && \
export KC_ENV="$(KC_ENV)" && \
export KC_APP_REALM_ADMIN_SECRET=$$(cat ./tmp/KC_APP_REALM_ADMIN_SECRET.txt) && \
uv run scripts/show-keycloak-client-jwks.py --client-id "$(CLIENT_ID)"
keycloak-match-client-public-keys: ## Match client public keys to private keys (client-signed JWT test-clients)
@echo "π Matching client public keys to private keys..."
@if ! ls $(KEYCLOAK_JWT_CLIENT_DIR)/*.public.yaml >/dev/null 2>&1; then \
echo "β Error: no client-signed JWT clients in $(KEYCLOAK_JWT_CLIENT_DIR)"; \
echo " βΉοΈ Hint: run 'make keycloak-generate-machine-clients' first"; \
exit 1; \
fi
@set -a && source .env && set +a && \
export KC_ENV="$(KC_ENV)" && \
export KC_APP_REALM_ADMIN_SECRET=$$(cat ./tmp/KC_APP_REALM_ADMIN_SECRET.txt) && \
for public_yaml in $(KEYCLOAK_JWT_CLIENT_DIR)/*.public.yaml; do \
client_id=$$(basename "$$public_yaml" .public.yaml); \
echo ""; \
echo "=== $$client_id ==="; \
uv run scripts/show-keycloak-client-jwks.py --client-id "$$client_id" && \
uv run scripts/validate-client-key-pair.py \
--clients-file "$(MACHINE_CLIENTS_EXTENDED_YAML)" \
--client-id "$$client_id" \
--key-file "$(KEYCLOAK_JWT_CLIENT_DIR)/$$client_id.private.pem" \
--kid "$$client_id" || exit 1; \
done
@echo ""
@echo "β
Client public keys matched to private keys!"
.get-client-credentials: ## Retrieve client credentials from Keycloak
@set -a && source .env && set +a && \
export KC_APP_REALM_ADMIN_SECRET=$$(cat ./tmp/KC_APP_REALM_ADMIN_SECRET.txt) && \
source ./keycloak/get-client-secret.sh && \
CA1_CLIENT_ID=sdep-test-ca.01 && KC_APP_REALM_CLIENT_ID=$$CA1_CLIENT_ID && get_client_secret && CA1_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
CA2_CLIENT_ID=sdep-test-ca.02 && KC_APP_REALM_CLIENT_ID=$$CA2_CLIENT_ID && get_client_secret && CA2_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
STR_CLIENT_ID=sdep-test-str.01 && KC_APP_REALM_CLIENT_ID=$$STR_CLIENT_ID && get_client_secret && STR_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
STA_CLIENT_ID=sdep-test-sta.01 && KC_APP_REALM_CLIENT_ID=$$STA_CLIENT_ID && get_client_secret && STA_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
LSA_CLIENT_ID=sdep-test-lsa.01 && KC_APP_REALM_CLIENT_ID=$$LSA_CLIENT_ID && get_client_secret && LSA_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
LMA_CLIENT_ID=sdep-test-lma.01 && KC_APP_REALM_CLIENT_ID=$$LMA_CLIENT_ID && get_client_secret && LMA_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
AMA_CLIENT_ID=sdep-test-ama.01 && KC_APP_REALM_CLIENT_ID=$$AMA_CLIENT_ID && get_client_secret && AMA_CLIENT_SECRET=$$KC_APP_REALM_CLIENT_SECRET && \
echo "export CA1_CLIENT_ID=$$CA1_CLIENT_ID" > ./tmp/.credentials && \
echo "export CA1_CLIENT_SECRET=$$CA1_CLIENT_SECRET" >> ./tmp/.credentials && \
echo "export CA2_CLIENT_ID=$$CA2_CLIENT_ID" >> ./tmp/.credentials && \
echo "export CA2_CLIENT_SECRET=$$CA2_CLIENT_SECRET" >> ./tmp/.credentials && \
echo "export STR_CLIENT_ID=$$STR_CLIENT_ID" >> ./tmp/.credentials && \
echo "export STR_CLIENT_SECRET=$$STR_CLIENT_SECRET" >> ./tmp/.credentials && \
echo "export STA_CLIENT_ID=$$STA_CLIENT_ID" >> ./tmp/.credentials && \
echo "export STA_CLIENT_SECRET=$$STA_CLIENT_SECRET" >> ./tmp/.credentials && \
echo "export LSA_CLIENT_ID=$$LSA_CLIENT_ID" >> ./tmp/.credentials && \
echo "export LSA_CLIENT_SECRET=$$LSA_CLIENT_SECRET" >> ./tmp/.credentials && \
echo "export LMA_CLIENT_ID=$$LMA_CLIENT_ID" >> ./tmp/.credentials && \
echo "export LMA_CLIENT_SECRET=$$LMA_CLIENT_SECRET" >> ./tmp/.credentials && \
echo "export AMA_CLIENT_ID=$$AMA_CLIENT_ID" >> ./tmp/.credentials && \
echo "export AMA_CLIENT_SECRET=$$AMA_CLIENT_SECRET" >> ./tmp/.credentials
##@ Backend
backend-up: .build .clean-stale ## Start backend + database migration
@echo "π Starting backend..."
$(DOCKER_COMPOSE) up -d backend
@echo "β
Backend started!"
@echo "βΉοΈ Run 'make status' to explore URLs"
backend-down: ## Stop and remove backend (including volumes)
@echo "π Stopping and removing backend (including volumes)..."
$(DOCKER_COMPOSE) stop backend
$(DOCKER_COMPOSE) rm -f -v backend
@echo "β
Backend stopped, removed, and volumes cleaned!"
backend-restart: backend-down backend-up ## Stop and restart backend
##@ Fullstack
up: .build .clean-stale ## Start postgres + keycloak + backend + load testdata
@echo "π Starting fullstack..."
$(DOCKER_COMPOSE) up -d
@echo "β
Fullstack started!"
@echo "π Configuring Keycloak..."
@$(MAKE) --no-print-directory keycloak-configure
@echo "β
Keycloak configured!"
@echo "π Initializing database..."
@$(MAKE) --no-print-directory postgres-drop-migrate-load
@echo "β
Database initialized!"
@echo "π Showing fullstack status..."
@$(MAKE) --no-print-directory status
@echo "β
Fullstack status shown!"
down: ## Stop and remove (including volumes)
@echo "π Stopping and removing fullstack (including volumes)..."
$(DOCKER_COMPOSE) down -v # Includes volume deletion
@echo "β
Fullstack stopped and removed!"
restart: down up ## Stop and start
status: ## Show status
@echo ""
@echo "π Containers:"
@$(DOCKER_COMPOSE) ps
@echo ""
@echo "π Use these URLs when containers are running:"
@set -a && source .env && set +a && \
printf " %-42s %s\n" "Backend API docs (version independent):" "$$BACKEND_BASE_URL/api/docs" && \
printf " %-42s %s\n" "Backend API docs (auth):" "$$BACKEND_BASE_URL/api/auth/v1/docs" && \
printf " %-42s %s\n" "Backend API docs (ca v1, v2):" "$$BACKEND_BASE_URL/api/ca/v1/docs $$BACKEND_BASE_URL/api/ca/v2/docs" && \
printf " %-42s %s\n" "Backend API docs (str v1, v2):" "$$BACKEND_BASE_URL/api/str/v1/docs $$BACKEND_BASE_URL/api/str/v2/docs" && \
printf " %-42s %s\n" "Backend API docs (lsa v2):" "$$BACKEND_BASE_URL/api/lsa/v2/docs" && \
printf " %-42s %s\n" "Backend API docs (lma v2):" "$$BACKEND_BASE_URL/api/lma/v2/docs" && \
printf " %-42s %s\n" "Backend API docs (ama v1):" "$$BACKEND_BASE_URL/api/ama/v1/docs" && \
printf " %-42s %s\n" "Backend API docs (sta v1, v2):" "$$BACKEND_BASE_URL/api/sta/v1/docs $$BACKEND_BASE_URL/api/sta/v2/docs" && \
printf " %-42s %s\n" "Backend health:" "$$BACKEND_BASE_URL/api/health" && \
printf " %-42s %s\n" "Keycloak:" "$$KC_BASE_URL/admin"
@echo ""
# Test data and idempotency. A plain test run cleans up after itself, so it is
# idempotent: everything it creates is named sdep-test-* (perf activities are
# sdep-test-perf-*) and postgres/clean-testrun.sql removes it afterwards. The "keep"
# variants are marked "not idempotent" because both leave their rows behind, so
# repeated keep-runs add up; the next run WITHOUT "keep" pre-cleans them.
#
# No keep variant survives beyond that next run, and none can: the perf fixtures hang
# off the sdep-test-ca.01 competent authority and the sdep-test-str.01 platform, and
# clean-testrun.sql deletes those accounts themselves. The foreign keys then take
# everything they own with them, whatever the rows are named. Use KEEP_TEST_DATA=true
# to inspect a run's data before the next test run, not as long-term storage.
#
# The correctness SLI does not depend on this: it samples during the run and verifies
# in the same run's summary hook (tests/performance/locustfile.py).
# See docs/PERFORMANCE_TESTS.md for the cleanup query itself.
##@ Tests (fullstack, integration)
.is-up: ## Check if services are running
@echo "π Checking if services are up..." && \
set -a && source .env && set +a && \
POSTGRES_STATUS=$$(docker inspect --format='{{.State.Health.Status}}' $$POSTGRES_CONTAINER_NAME 2>&1 | grep -v "^Error" || echo "not-running"); \
KC_STATUS=$$(docker inspect --format='{{.State.Status}}' $$KC_CONTAINER_NAME 2>&1 | grep -v "^Error" || echo "not-running"); \
BACKEND_STATUS=$$(docker inspect --format='{{.State.Health.Status}}' $$BACKEND_CONTAINER_NAME 2>&1 | grep -v "^Error" || echo "not-running"); \
ALL_UP=true; \
echo ""; \
printf " %-15s %s\n" "Postgres:" "$$POSTGRES_STATUS"; \
if [ "$$POSTGRES_STATUS" != "healthy" ]; then ALL_UP=false; fi; \
printf " %-15s %s\n" "Keycloak:" "$$KC_STATUS"; \
if [ "$$KC_STATUS" != "running" ]; then ALL_UP=false; fi; \
printf " %-15s %s\n" "Backend:" "$$BACKEND_STATUS"; \
if [ "$$BACKEND_STATUS" != "healthy" ]; then ALL_UP=false; fi; \
echo ""; \
if [ "$$ALL_UP" = "true" ]; then \
echo "β
All services are up and healthy!"; \
exit 0; \
else \
echo "β Some services are not healthy!"; \
echo ""; \
echo "Please start all services first with:"; \
echo " make up"; \
echo ""; \
exit 1; \
fi
.ensure-up: ## Start the stack only if it is not already running and healthy
@$(MAKE) --no-print-directory .is-up >/dev/null 2>&1 || $(MAKE) --no-print-directory up
# The test-<suite> targets run one suite of tests/suites.txt (DEV column), see scripts/run-suite.sh.
test-smoke: .ensure-up ## Test smoke (audit-excluded, no auth needed; no test data created)
@set -a && source ./.env && set +a && \
scripts/run-suite.sh smoke
# Quiet: one π line per suite as it finishes (see run_suite in scripts/run-tests.sh), then the summary.
test-full: .ensure-up ## Test fullstack (quiet)
@set -a && source ./.env && set +a && \
set -o pipefail && \
$(MAKE) --no-print-directory test-full-verbose 2>&1 | sed -un '/^π /p; /^ββ TEST RESULTS/,$$p'
test-full-keep: .ensure-up .get-client-credentials ## Test fullstack (quiet, keep generated test-data; not idempotent, adds up until a test run without "keep", or until postgres-clean-testrun)
@set -a && source ./.env && set +a && \
set -o pipefail && \
KEEP_TEST_DATA=true $(CURDIR)/scripts/run-tests.sh 2>&1 | sed -un '/^π /p; /^ββ TEST RESULTS/,$$p'
test-full-verbose: .ensure-up .get-client-credentials ## Test fullstack (verbose)
@$(CURDIR)/scripts/run-tests.sh
# Every runner reads tests/suites.txt, so a test that is missing there never runs.
test-suites: ## Test that tests/suites.txt lists every test and is well-formed (offline)
@echo "π§ͺ Checking tests/suites.txt..."
@uv run --script tests/test_suites.py
test-ca: .ensure-up .get-client-credentials # Helper - Test only CA endpoints
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh ca
test-str: .ensure-up .get-client-credentials # Helper - Test only STR endpoints
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh str
test-sta: .ensure-up .get-client-credentials # Helper - Test only STA endpoints
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh sta
test-lsa: .ensure-up .get-client-credentials # Helper - Test only LSA endpoints (incl. the listing lifecycle)
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh lsa
test-lma: .ensure-up .get-client-credentials # Helper - Test only LMA endpoints
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh lma
test-ama: .ensure-up .get-client-credentials # Helper - Test only AMA endpoints
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh ama
# test_auth_client_jwt is not in a suite: it provisions its own client-jwt clients here.
test-security: .ensure-up .get-client-credentials # Helper - Test only security (headers, unauthorized, credentials)
@set -a && source ./.env && source ./tmp/.credentials && set +a && \
scripts/run-suite.sh security && \
echo "Testing client-signed-JWT credentials..." && \
JWT_PROVISION_CLIENTS=true uv run --script tests/test_auth_client_jwt.py && \
echo "β
Security tested!"
##@ Tests (migrations)
.postgres-up-unless-ci: ## Start postgres, unless running in CI (which provides it as a service)
@if [ -z "$$CI" ]; then \
$(MAKE) --no-print-directory postgres-up; \
fi
test-migrations: .postgres-up-unless-ci ## Test alembic migrations in postgresql
@echo "π§ͺ Running migration tests..."
@cd backend && uv run python scripts/wait_for_postgres.py
@$(MAKE) -C backend --no-print-directory upgrade
@cd backend && PYTHONPATH=. uv run python scripts/check_db_matches_models.py
@uv run --script tests/test_postgres_check_constraints.py
@echo "β
Migration tests completed!"
##@ Tests (performance)
PERF_ACTIVITIES_TARGET ?= 5000
PERF_MAX_DURATION_SECONDS ?= 300
PERF_BATCH_SIZE ?= 1000
PERF_USERS ?= 10
PERF_RAMP_UP ?= 1
KEEP_TEST_DATA ?= false
PERF_STOP_ON_TARGET ?= true
PERF_AUTO_CONFIRM ?= false
PERF_ENV = PERF_ACTIVITIES_TARGET=$(PERF_ACTIVITIES_TARGET) \
PERF_USERS=$(PERF_USERS) \
PERF_RAMP_UP=$(PERF_RAMP_UP) \
PERF_MAX_DURATION_SECONDS=$(PERF_MAX_DURATION_SECONDS) \
PERF_BATCH_SIZE=$(PERF_BATCH_SIZE) \
KEEP_TEST_DATA=$(KEEP_TEST_DATA) \
PERF_STOP_ON_TARGET=$(PERF_STOP_ON_TARGET) \
PERF_AUTO_CONFIRM=$(PERF_AUTO_CONFIRM)
test-perf: .ensure-up .get-client-credentials ## Test performance (STR activities)
@$(PERF_ENV) $(CURDIR)/scripts/run-tests-perf.sh
@$(MAKE) --no-print-directory postgres-count
test-perf-keep: .ensure-up .get-client-credentials ## Same as test-perf, keep generated test-data (not idempotent, adds up until a test run without "keep", or until postgres-clean-testrun)
@$(PERF_ENV) KEEP_TEST_DATA=true $(CURDIR)/scripts/run-tests-perf.sh
@$(MAKE) --no-print-directory postgres-count
test-perf-verbose: .ensure-up .get-client-credentials ## Same as test-perf, with periodic Locust stats
@$(PERF_ENV) PERF_VERBOSE=true $(CURDIR)/scripts/run-tests-perf.sh
##@ Tests (security)
# No .ensure-up here on purpose: the malware test talks directly to ClamAV (not the
# backend API), so it only needs its own clamav container, started below (idempotent
# if `make up` already started it). Bringing up the full stack would run a compose
# build/up that fails where there is no compose stack β this keeps test-malware able
# to run standalone in a CI/CD environment (which provides ClamAV as a service).
test-malware: ## Test malware
@echo "π§ͺ Running malware scanning tests..."
@if [ -z "$$CI" ]; then \
$(DOCKER_COMPOSE) up -d clamav; \
CLAMAV_CONTAINER_ID="$$($(DOCKER_COMPOSE) ps -q clamav)"; \
CLAMAV_HEALTH="$$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$$CLAMAV_CONTAINER_ID")"; \
if [ "$$CLAMAV_HEALTH" = "unhealthy" ]; then \
echo "β ClamAV container is unhealthy; malware scan tests cannot run."; \
echo ""; \
echo "Docker healthcheck output:"; \
docker inspect --format '{{range .State.Health.Log}}{{println .Output}}{{end}}' "$$CLAMAV_CONTAINER_ID"; \
echo "Recent ClamAV logs:"; \
$(DOCKER_COMPOSE) logs --tail=40 clamav; \
exit 1; \
fi; \
fi
uv run --script tests/malware/test_malware_scan.py
@echo "β
Malware scanning tests completed!"
test-cve: export DOCKER_DEFAULT_PLATFORM := linux/amd64
# Scan a throwaway tag (local/sdep-backend:trivy-scan) instead of the dev image.
# The fresh --no-cache build below otherwise replaces local/sdep-backend:latest,
# which would make the next `make up` needlessly recreate the backend container.
test-cve: export BACKEND_IMAGE_VERSION := trivy-scan
test-cve: ## Test the CVE allowlist by really scanning the SDEP backend image (fails on failure > fix or explain in docs/CVE_EXPLAINS.md, your own allowlist, not published)
@echo "π Scanning backend image for CVEs..."
@echo ""
@# Always build fresh (--pull --no-cache) so apt-get upgrade fetches current
@# Debian security patches; a cached layer would mask already-fixed CVEs.
@$(DOCKER_COMPOSE) build --pull --no-cache backend
@mkdir -p tmp/trivy-scan
@docker save "$(BACKEND_IMAGE_NAME):$(if $(BACKEND_IMAGE_VERSION),$(BACKEND_IMAGE_VERSION),latest)" -o tmp/trivy-scan/backend-image.tar
@# Stage 1 - scan in the Trivy image (writes tmp/trivy-scan/trivy-results.json).
@# Pass host UID/GID so the container restores ownership of its report.
@$(DOCKER_COMPOSE) run --rm \
-e TRIVY_INPUT=tmp/trivy-scan/backend-image.tar \
-e HOST_UID=$$(id -u) -e HOST_GID=$$(id -g) \
run-trivy-scan
@# Stage 2 - reconcile the report against the allowlist (plain Python, on the host).
@uv run --script scripts/check_cve_allowlist.py tmp/trivy-scan/trivy-results.json
@echo ""
@echo "β
CVE scan completed!"
# Smoketest for scripts/check_cve_allowlist.py itself, plus an offline plausible-year sanity
# check for CVE ids. Feeds the script fake scan reports, so it runs without Docker or an image.
test-cve-offline: ## Test the CVE allowlist script with fake reports, check CVE ids in the docs
@echo "π§ͺ Testing the CVE allowlist script with fake reports (no image scan)..."
@uv run --script tests/test_cve_ids.py
@uv run --script tests/test_trivy_allowlist.py
@echo "β
CVE allowlist script tests completed!"
##@ Tests (all)
test: ## Test fullstack + migrations + performance + security (malware)
@echo "π§ͺ Running: test-full + test-migrations + test-perf + test-malware"
@echo ""
@$(MAKE) --no-print-directory test-full
@$(MAKE) --no-print-directory test-migrations
@$(MAKE) --no-print-directory test-perf PERF_AUTO_CONFIRM=true
@$(MAKE) --no-print-directory test-malware
@echo ""
@echo "β
Completed"
@echo ""
@$(MAKE) --no-print-directory postgres-count
test-keep: ## Test fullstack + migrations + performance + security (malware); keep generated test-data (not idempotent, similar to test-full-keep and test-perf-keep)
@echo "π§ͺ Running (keep test-data): test-full-keep + test-migrations + test-perf-keep + test-malware"
@echo ""
@$(MAKE) --no-print-directory test-full-keep
@$(MAKE) --no-print-directory test-migrations
@$(MAKE) --no-print-directory test-perf-keep PERF_AUTO_CONFIRM=true
@$(MAKE) --no-print-directory test-malware
@echo ""
@echo "β
Completed (test-data kept)"
@echo ""
@$(MAKE) --no-print-directory postgres-count
##@ Markdown
# Three tools do the work, each from its own ecosystem:
# - markdownlint-cli2 (Node) checks the house rules; run on demand via npx
# - mdformat (Python) rewrites the file; run on demand via uvx, with
# mdformat-gfm for table alignment and the local
# mdformat-house plugin for thematic breaks as "---"
# - format_tree_visualizations.py (Python, in the repo) aligns the `# comment`
# column in directory-tree code blocks (--check to lint)
# The first two are not vendored or installed into the repo: npx and uvx fetch the pinned
# versions below on first use and cache them. All shared config lives under docs/markdown-tooling/:
# - .markdownlint-cli2.jsonc : markdownlint config (loaded via --config)
# - markdownlint-rules/ : custom .cjs house rules
# - mdformat/ : local mdformat plugin (thematic breaks as "---")
# - format_tree_visualizations.py : tree comment alignment
# Both are ordinary processes, so `make md-lint` works locally and could equally run as a
# CI/CD pipeline gate: it only needs Node/npm and Python/uv on PATH (a toolbox image with
# both is enough) and, unlike the container-based checks, no Docker daemon or service.
MARKDOWN_TOOLING := docs/markdown-tooling
MARKDOWNLINT_VERSION := 0.18.1
MARKDOWNLINT := npx --yes markdownlint-cli2@$(MARKDOWNLINT_VERSION) --config $(MARKDOWN_TOOLING)/.markdownlint-cli2.jsonc
MARKDOWNLINT_FIX := $(MARKDOWNLINT) --fix
MDFORMAT := uvx --from mdformat==1.0.0 --with mdformat-gfm==1.0.0 --with ./$(MARKDOWN_TOOLING)/mdformat mdformat --number
TREEFORMAT := python3 $(MARKDOWN_TOOLING)/format_tree_visualizations.py
# Every public *.md in the repo (scripts/public_files.sh: git-known, minus the private
# paths marked export-ignore in .gitattributes), minus the "ignores" of
# .markdownlint-cli2.jsonc (single source for both tools). "=" so the shell commands
# only run for the md-* targets.
MD_FILES = $(shell ./scripts/public_files.sh '*.md')
MDFORMAT_EXCLUDES = $(shell $(MARKDOWN_TOOLING)/mdformat-excludes.sh)
md-lint: ## Lint markdown
@echo "π Linting Markdown..."
@$(MARKDOWNLINT) $(MD_FILES)
@$(MDFORMAT) --check $(MDFORMAT_EXCLUDES) $(MD_FILES)
@$(TREEFORMAT) --check $(MD_FILES)
@echo "β
Markdown lint passed!"
md-format: ## Format markdown
@echo "π Formatting Markdown..."
@$(MARKDOWNLINT_FIX) $(MD_FILES) || true
@$(MDFORMAT) $(MDFORMAT_EXCLUDES) $(MD_FILES)
@$(TREEFORMAT) $(MD_FILES)
@echo "β
Markdown formatted!"
# Scans the whole docs/ directory, not the public MD_FILES of md-lint: a dead link
# in a private document is just as broken. Shared with sdep-app's sibling repositories.
md-validate-links: ## Check that every relative Markdown link and heading anchor resolves
@echo "π Checking dead links..."
@python3 scripts/check_dead_links.py --quiet docs README.md CHANGELOG.md
@echo "β
No dead links!"
##@ Spelling
# codespell (Python, run on demand via uvx like mdformat): common typos plus British English,
# Oxford spelling. For config, skipped files and accepted words, see .codespellrc.
# Checks every file git knows (private paths too), binary files are skipped.
# Fix with `codespell -w <file>`.
CODESPELL := uvx codespell@2.4.3
spell-check: ## Check spelling in src and doc (typos + British English, Oxford spelling)
@echo "π Checking spelling..."
@git ls-files --cached --others --exclude-standard -z | xargs -0 $(CODESPELL)
@echo "β
Spelling check passed!"
##@ All
# Gate order, shared by all, ci-gate and dod: cheap and Docker-only steps first (markdown,
# backend test, CVE), the stack-bound suites last, so a failure that needs a docs or allowlist
# fix costs seconds, not the full cycle.
all: ## Markdown format/lint + spell-check + backend test + CVE offline + suites + CVE scan + migrations + fullstack + performance + malware
@echo "π§ͺ Running: md-format + md-lint + spell-check + backend test + test-cve-offline + test-suites + test-cve + test-migrations + test-full + test-perf + test-malware"
@echo ""
@$(MAKE) --no-print-directory md-format
@$(MAKE) --no-print-directory md-lint
@$(MAKE) --no-print-directory spell-check
@$(MAKE) -C backend --no-print-directory test
@$(MAKE) --no-print-directory test-cve-offline
@$(MAKE) --no-print-directory test-suites
@$(MAKE) --no-print-directory test-cve
@$(MAKE) --no-print-directory test-migrations
@$(MAKE) --no-print-directory test-full
@$(MAKE) --no-print-directory test-perf PERF_AUTO_CONFIRM=true
@$(MAKE) --no-print-directory test-malware
@echo ""
@echo "β
Completed"
# Mirrors the required continuous integration checks on push
# test:backend -> make -C backend test (pytest + coverage)
# test:database-migrations -> test-migrations
# test:malware -> test-malware
# test:suites -> test-suites
# trivy:scan + trivy:gate -> test-cve (scan the image, then check the report against the allowlist)
# markdown:lint -> md-lint
# spelling:check -> spell-check
# Split by cost, not by pipeline shape: the image scan is the slow check and lives here, while
# the offline smoketest of the allowlist script lives in `make all`. The pipeline's
# test:cve-offline job therefore has no counterpart in this target - run `make all` (or
# `make test-cve-offline`) when touching scripts/check_cve_allowlist.py or the CVE documents.
# Note: CI does NOT run the fullstack (test-full) suite on push; that lives in `make all`/`make test`.
# test-perf is included here by choice, so a local run exercises the bulk path before pushing,
# even though the pipeline does not. Consider to keep this target in sync with continuous integration (pipeline) jobs.
ci-gate: ## Markdown lint + spell-check + backend test + suites + CVE scan + migrations + performance + malware
@echo "π§ͺ Running CI checks (mirrors pipeline gates): md-lint + spell-check + backend test + test-suites + test-cve + test-migrations + test-perf + test-malware"
@echo ""
@$(MAKE) --no-print-directory md-lint
@$(MAKE) --no-print-directory spell-check
@$(MAKE) -C backend --no-print-directory test
@$(MAKE) --no-print-directory test-suites
@$(MAKE) --no-print-directory test-cve
@$(MAKE) --no-print-directory test-migrations
@$(MAKE) --no-print-directory test-perf PERF_AUTO_CONFIRM=true
@$(MAKE) --no-print-directory test-malware
@echo ""
@echo "β
CI checks completed"
# Definition of Done = docs checks + API snapshots/diff + `all` (with the keep sequences):
# docs checks -> forbidden references, dead links, architecture tree, docs consistency, test suites, changelog
# API -> api-snapshot-update + api-diff-update (before the backend test that freezes them)
# all -> as the `all` target, with the stack started and Keycloak provisioned before the
# suites, and the fullstack and performance suites each run clean, keep, keep, clean:
# a keep run must not break the next clean run, and a clean run must remove what two
# keep runs left
# One line per step on the terminal, full output in tmp/dod/<step>.log, stops at the first
# failure. The manual review items stay in the developer tooling (private, see .gitattributes).
dod: ## Docs checks (e.g. dead links, architecture tree) + API snapshots + API diffs + All/all (with fullstack and performance tests running a clean/keep/keep/clean sequence)
@./scripts/run-dod.sh
# Resume after a failed dod: the cheap steps rerun, the stack-bound steps that passed are skipped
# (see scripts/run-dod.sh for the markers and the refusal when backend/ changed since).
dod-continue: ## Continue a failed dod from its failed step (cheap steps rerun, passed stack-bound steps are skipped)
@DOD_CONTINUE=1 ./scripts/run-dod.sh
##@ Logs
postgres-logs: ## Show postgres logs
$(DOCKER_COMPOSE) logs -f postgres
keycloak-logs: ## Show keycloak logs
$(DOCKER_COMPOSE) logs -f keycloak
backend-logs: ## Show backend logs
$(DOCKER_COMPOSE) logs -f backend
dbgate-logs: ## Show dbgate logs (optional)
@touch /tmp/dbgate.log
@echo "π Tailing /tmp/dbgate.log (Ctrl+C to stop)"
@tail -f /tmp/dbgate.log
fullstack-logs: ## Show fullstack logs (postgres + keycloak + backend + dbgate/optional)
$(DOCKER_COMPOSE) logs -f
##@ Help
help: ## Show help
@echo "π€ Make"
@echo ""
@echo "Available commands:"
@echo ""
@echo "Usage:"
@printf " make \033[36m<target>\033[0m\n"
@echo ""
@echo " π‘ All targets are idempotent (unless stated otherwise)"
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)