Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1589 lines (1451 loc) · 73.9 KB
/
Copy pathMakefile
File metadata and controls
1589 lines (1451 loc) · 73.9 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
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# === BNK Forge — Commands ===
# Usage: make deploy — build + deploy (auto-detects Linux vs macOS networking)
# make install — first-time server setup (builds from scratch)
# make update — pull latest + rebuild + restart (keeps all data)
# make test — run all tests locally
# make help — see all commands
#
# Requires:
# - Docker with BuildKit (default since Docker 23+)
# - backend/.venv with dependencies installed (for local tests)
# - frontend-v2/node_modules installed (for local tests)
SHELL := /bin/bash
# OPT-001: Ensure BuildKit is enabled for cache mount support
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# ─── Platform-aware compose command ──────────────────────────────────────────
# Auto-detects Linux vs macOS and picks the right networking:
# Linux → network_mode: host (services bind directly to host ports)
# macOS → bridge networking with port mappings (docker-compose.local.yml overlay)
#
# SAFETY: Running bridge networking on Linux creates iptables rules that can
# kill SSH connections permanently when net.bridge.bridge-nf-call-iptables = 1
# (the default on most distros). Requires BMC/IPMI reset to recover.
UNAME_S := $(shell uname -s)
# WSL2 reports "Linux" but runs Docker Desktop, which behaves like macOS for
# networking — host-mode containers bind inside the docker-desktop WSL2 distro,
# not the user's distro, so the local overlay (bridge + published ports) is
# what makes a Vite dev server reach the backend.
IS_WSL := $(shell grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null && echo 1)
ifeq ($(UNAME_S),Darwin)
COMPOSE := docker compose -f docker-compose.yml -f docker-compose.local.yml
else ifeq ($(IS_WSL),1)
COMPOSE := docker compose -f docker-compose.yml -f docker-compose.local.yml
else
COMPOSE := docker compose
endif
# ─── CI / Local differentiation ─────────────────────────────────────────────
# GitHub Actions sets CI=true automatically. These variables control output
# format differences. The actual commands are identical in both contexts.
ifdef CI
# CI: XML artifacts for GitHub Actions upload. SUITE is set per-target below.
PYTEST_COV_REPORT = --cov-report=xml:coverage-$(SUITE).xml
PYTEST_JUNIT = --junitxml=junit-$(SUITE).xml
BACKEND_VENV :=
BACKEND_VENV_ROOT :=
OPERATOR_VENV :=
BACKEND_PREREQ :=
OPERATOR_PREREQ :=
FRONTEND_PREREQ :=
else
# Local: terminal output, venv activation
PYTEST_COV_REPORT := --cov-report=term-missing
PYTEST_JUNIT :=
BACKEND_VENV := source .venv/bin/activate &&
BACKEND_VENV_ROOT := source backend/.venv/bin/activate &&
OPERATOR_VENV := source .venv/bin/activate &&
BACKEND_PREREQ := | backend/.venv/bin/activate
OPERATOR_PREREQ := | bnk-operator/.venv/bin/activate
FRONTEND_PREREQ := | frontend-v2/node_modules
endif
PYTEST_BASE = python -m pytest
# --cov enables coverage; --cov-fail-under=0 overrides pyproject.toml's fail_under
# so individual suite runs don't fail on partial coverage. The full suite
# coverage threshold is enforced by `make coverage-backend`.
PYTEST_COV = --cov --cov-fail-under=0
# ─── awsbnkctl CLI binary (cli-bnkctl engine) ────────────────────────────────
# The celery worker shells out to a pinned awsbnkctl release binary that is
# bind-mounted at ./bin/awsbnkctl (see docker-compose.yml x-worker-volumes).
# `make fetch-awsbnkctl` downloads + checksum-verifies the pinned linux/amd64
# release instead of requiring a hand-run `go build` on the host.
AWSBNKCTL_VERSION ?= 0.9.0-rc1
AWSBNKCTL_REPO ?= JLCode-tech/awsbnkctl
AWSBNKCTL_BIN := bin/awsbnkctl
AWSBNKCTL_STAMP := bin/.awsbnkctl-$(AWSBNKCTL_VERSION).stamp
.PHONY: install update status logs \
test test-backend test-backend-unit test-backend-component test-backend-legacy test-frontend \
test-proxy test-operator test-db test-contracts test-e2e test-e2e-tier1 test-e2e-tier2 \
test-integration test-integration-full build-frontend-check smoke-mcp-live mcp-readiness mcp-recreate \
lint lint-backend lint-frontend shellcheck coverage quick-check version-check pre-push push install-hooks setup-hooks \
dev-setup security-audit docker-check docker-verify docker-validate \
openapi openapi-types openapi-check openapi-types-check typecheck-backend typecheck-frontend \
build build-retry build-backend build-frontend build-worker build-agent build-all \
fetch-awsbnkctl \
up down restart deploy deploy-backend deploy-frontend upgrade-safe \
clean clean-docker check-disk setup-cleanup-cron check-migrations \
commit-lint script-selftests ci-gates secret-scan artifact-network-selftest \
test-upgrade dist push-images push-customer-build buildx-setup publish-signed help
# ─── Quick Start Commands ────────────────────────────────────────────────────
#
# These are the main entry points for users:
# make install — first-time setup (builds everything from scratch)
# make update — pull latest + rebuild + restart (keeps all data)
# make status — show container health and versions
#
# First-time installation: clean slate, build, configure, start
# This is the only entry point for new installations. No separate scripts.
install: _install-clean _install-override _install-build _install-start _install-info
# --- Install sub-targets (not meant to be called directly) ---
_install-clean:
@echo ""
@echo "========================================="
@echo " BNK Forge — First-Time Installation"
@echo "========================================="
@echo ""
@echo "Stopping and removing existing containers..."
@docker compose down 2>/dev/null || true
@echo "Removing existing volumes..."
@docker volume ls -q | grep bnk-forge | xargs docker volume rm 2>/dev/null || echo " No volumes to remove"
@echo "Removing existing images..."
@docker images | grep bnk-forge | awk '{print $$1":"$$2}' | xargs docker rmi 2>/dev/null || echo " No images to remove"
_install-override:
@echo ""
@echo "Configuring GUI Upgrade..."
@printf '%s\n' \
'# Auto-generated by make install — enables GUI Upgrade feature' \
'# SEC-004: Docker socket grants full Docker daemon access (root-equivalent).' \
'# Remove this file if you do not need the GUI "Upgrade Now" button.' \
'' \
'services:' \
' backend:' \
' environment:' \
' HOST_REPO_PATH: $(CURDIR)' \
' volumes:' \
' - /var/run/docker.sock:/var/run/docker.sock' \
> docker-compose.override.yml
@echo " HOST_REPO_PATH=$(CURDIR)"
@echo " ✓ Created docker-compose.override.yml"
_install-build:
@echo ""
@echo "Building all images (first build: ~3 min, cached: ~30s)..."
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build
_install-start: ensure-artifact-network
@echo ""
@echo "Starting infrastructure (postgres, redis)..."
@docker compose up -d postgres redis
@echo " Waiting for database..."
@for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do \
if docker compose exec -T postgres pg_isready -U bnkforge > /dev/null 2>&1; then \
echo " ✓ Database ready"; \
break; \
fi; \
sleep 1; \
done
@echo ""
@echo "Fixing volume permissions..."
@PROJECT=$$(basename "$(CURDIR)"); \
for v in bnk-forge-data bnk-forge-keys bnk-forge-agent-token state_data helm_cache helm_config helm_charts workspace_data; do \
docker volume create \
--label com.docker.compose.project=$${PROJECT} \
--label com.docker.compose.volume=$${v} \
"$${PROJECT}_$${v}" >/dev/null; \
done; \
docker run --rm \
-v "$${PROJECT}_bnk-forge-data:/app/projects" \
-v "$${PROJECT}_bnk-forge-keys:/app/keys" \
-v "$${PROJECT}_bnk-forge-agent-token:/app/agent-token" \
-v "$${PROJECT}_state_data:/app/state" \
-v "$${PROJECT}_helm_cache:/home/bnkforge/.cache/helm" \
-v "$${PROJECT}_helm_config:/home/bnkforge/.config/helm" \
-v "$${PROJECT}_helm_charts:/app/helm_charts" \
-v "$${PROJECT}_workspace_data:/app/workspaces" \
alpine:latest sh -c " \
mkdir -p /app/projects /app/keys /app/agent-token /app/state /app/helm_charts /app/workspaces \
/home/bnkforge/.cache/helm /home/bnkforge/.config/helm && \
chown -R 1000:1000 /app/projects /app/keys /app/agent-token /app/state /app/helm_charts \
/app/workspaces /home/bnkforge" \
2>/dev/null && echo " ✓ Volume permissions configured" \
|| echo " ⚠ Could not pre-configure permissions"
@echo ""
@echo "Starting all services..."
@docker compose up -d
@echo ""
$(call wait-healthy,12,5)
_install-info:
@echo ""
@docker compose ps
@echo ""
@HOST_IP=$$(ip route get 1 2>/dev/null | awk '{print $$7; exit}' \
|| hostname -I 2>/dev/null | awk '{print $$1}' \
|| echo "localhost"); \
if [ -z "$$HOST_IP" ] || [ "$$HOST_IP" = "127.0.0.1" ]; then HOST_IP="localhost"; fi; \
echo "========================================="; \
echo " ✅ Installation complete!"; \
echo ""; \
echo " Version: $$(cat VERSION 2>/dev/null || echo 'unknown')"; \
echo ""; \
if [ "$$HOST_IP" = "localhost" ]; then \
echo " Open: https://localhost:8443"; \
else \
echo " Open: https://$$HOST_IP:8443"; \
echo " (accept the self-signed certificate warning)"; \
fi; \
echo ""; \
echo " Login: admin (password: DEFAULT_ADMIN_PASSWORD if set, else the generated one at /app/keys/initial_admin_password — change on first login)"; \
echo ""; \
echo " Next steps:"; \
echo " 1. Change your password on first login"; \
echo " 2. Browse deployable blueprints in Build → Catalog"; \
echo " 3. Create your first project in Build → Projects"; \
echo "========================================="
# Pull latest code, rebuild, and restart (non-destructive — keeps all data)
update:
@echo ""
@echo "========================================="
@echo " BNK Forge — Update"
@echo "========================================="
@echo ""
@./upgrade.sh
# Show container health, versions, and service status
status:
@echo ""
@echo "========================================="
@echo " BNK Forge — Status"
@echo "========================================="
@echo ""
@echo "Version: $$(cat VERSION 2>/dev/null || echo 'unknown')"
@echo "Commit: $$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
@echo ""
@$(COMPOSE) ps 2>/dev/null || echo "Docker Compose not running"
@echo ""
@echo "Health check:"
@curl -sf http://localhost:8000/api/system/health 2>/dev/null | python3 -m json.tool 2>/dev/null \
|| curl -sf https://localhost:8443/api/system/health -k 2>/dev/null | python3 -m json.tool 2>/dev/null \
|| echo " Backend not reachable (containers may not be running)"
# ─── Docker Build (OPT-001: cached builds) ──────────────────────────────────
#
# IMPORTANT: Do NOT use --no-cache for normal code changes!
# BuildKit caches pip/npm downloads across builds. Layer caching handles code.
# Only use `make build-clean` when requirements.txt or tool versions change.
#
# Build all app images — backend API, workers, beat, frontend
# OPT-002: Single docker compose build parallelizes independent stages via BuildKit
build:
@echo ""
@echo "=== Building all app images (parallel) ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build backend celery-worker celery-beat frontend forge-agent
@echo ""
@echo "========================================="
@echo " Build complete (cached)"
@echo "========================================="
# Same as `build`, but retries on transient download failures. Use this for
# from-scratch builds on DLP-managed workstations: the github.com tool
# downloads use Docker `ADD` (see docs/adr/D-035), which has no built-in retry,
# so a transient CDN/TLS blip aborts the whole build. BuildKit's layer cache
# makes each retry cheap (only the failed layer re-runs). Tune with
# RETRY_ATTEMPTS / RETRY_DELAY, e.g. `make build-retry RETRY_ATTEMPTS=5`.
build-retry:
@echo ""
@echo "=== Building all app images (parallel, with retry) ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 ./scripts/retry.sh -- docker compose build backend celery-worker celery-beat frontend forge-agent
@echo ""
@echo "========================================="
@echo " Build complete (cached)"
@echo "========================================="
# Build just the API image (backend code changes)
build-backend:
@echo ""
@echo "=== Building backend (API) ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 $(COMPOSE) build backend
@echo " ✓ Backend image built"
# Build just the frontend image
build-frontend:
@echo ""
@echo "=== Building frontend ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 $(COMPOSE) build frontend
@echo " ✓ Frontend image built"
# Fetch the pinned awsbnkctl release binary (linux/amd64) for the worker mount.
# Idempotent: a version stamp file short-circuits re-download. Override the
# version with `make fetch-awsbnkctl AWSBNKCTL_VERSION=x.y.z`.
fetch-awsbnkctl: $(AWSBNKCTL_STAMP)
$(AWSBNKCTL_STAMP):
@echo ""
@echo "=== Fetching awsbnkctl $(AWSBNKCTL_VERSION) (linux/amd64) ==="
@command -v gh >/dev/null 2>&1 || { echo " ✗ GitHub CLI (gh) is required to download the release"; exit 1; }
@mkdir -p bin
@tmp="$$(mktemp -d)"; \
asset="awsbnkctl_$(AWSBNKCTL_VERSION)_linux_amd64.tar.gz"; \
echo " Downloading $$asset + checksums.txt"; \
gh release download "v$(AWSBNKCTL_VERSION)" --repo "$(AWSBNKCTL_REPO)" \
--pattern "$$asset" --pattern "checksums.txt" --dir "$$tmp" --clobber || { rm -rf "$$tmp"; exit 1; }; \
echo " Verifying checksum"; \
( cd "$$tmp" && grep " $$asset$$" checksums.txt | shasum -a 256 -c - ) || { echo " ✗ checksum mismatch"; rm -rf "$$tmp"; exit 1; }; \
echo " Extracting awsbnkctl -> $(AWSBNKCTL_BIN)"; \
tar -xzf "$$tmp/$$asset" -C "$$tmp" awsbnkctl || { rm -rf "$$tmp"; exit 1; }; \
install -m 0755 "$$tmp/awsbnkctl" "$(AWSBNKCTL_BIN)"; \
rm -rf "$$tmp"
@rm -f bin/.awsbnkctl-*.stamp
@touch "$(AWSBNKCTL_STAMP)"
@echo " ✓ awsbnkctl $(AWSBNKCTL_VERSION) ready at $(AWSBNKCTL_BIN)"
# Build worker image (REQUIRED for any backend code change — workers run tasks/services)
# fetch-awsbnkctl is opt-in, not a hard prerequisite: it needs `gh` authenticated against
# AWSBNKCTL_REPO (a personal namespace today), so it must not block backend builds/deploys
# for contributors not touching the cli-bnkctl engine. We still attempt it automatically
# (non-fatal) so it "just works" for anyone who does have access; on failure we warn and
# continue the worker build without the binary — the cli-bnkctl engine will fail-soft at
# deploy time (see cli_bnkctl_module_seeder binary-presence gate) rather than blocking here.
build-worker:
@$(MAKE) fetch-awsbnkctl || echo " ⚠ Skipping awsbnkctl fetch ($(AWSBNKCTL_REPO) unavailable — install/auth 'gh' to enable the cli-bnkctl engine). Run 'make fetch-awsbnkctl' manually once you have access."
@echo ""
@echo "=== Building worker (includes CLI tools — slower) ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build celery-worker
@echo " ✓ Worker image built"
# Build the built-in benchmark agent image
build-agent:
@echo ""
@echo "=== Building forge-agent (built-in benchmark agent) ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build forge-agent
@echo " ✓ forge-agent image built"
# Build MCP server image
build-mcp:
@echo ""
@echo "=== Building MCP server ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build mcp
@echo " ✓ MCP server image built"
# Build ALL images (backend API + worker + beat + frontend + proxy + mcp + agent)
build-all:
@echo ""
@echo "=== Building ALL images ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build
@echo " ✓ All images built"
# Force rebuild without cache (only for requirements.txt / tool version changes)
build-clean:
@echo ""
@echo "=== Clean rebuild (no cache) — this will take several minutes ==="
BUILDX_NO_DEFAULT_ATTESTATIONS=1 docker compose build --no-cache
@echo " ✓ Clean rebuild complete"
# ─── Docker Deploy (build + restart + verify) ───────────────────────────────
#
# All deploy/runtime targets use $(COMPOSE) which auto-detects:
# Linux → host networking (docker compose)
# macOS → bridge networking (docker compose -f ... -f docker-compose.local.yml)
# Reusable health-check: polls backend /api/system/health.
# Usage: $(call wait-healthy,ATTEMPTS,SLEEP_SECONDS)
# e.g. $(call wait-healthy,12,5) → 12 attempts, 5s apart (60s max)
define wait-healthy
@echo "=== Waiting for health checks... ==="
@sleep $(2)
@for i in $$(seq 1 $(1)); do \
if curl -sf http://localhost:8000/api/system/health > /dev/null 2>&1; then \
echo " ✓ Backend healthy"; \
break; \
fi; \
if [ "$$i" = "$(1)" ]; then \
echo " ⚠ Backend did not become healthy within $$(( $(1) * $(2) ))s"; \
echo " Check logs: docker logs bnk-forge-backend"; \
else \
echo " Waiting... ($$i/$(1))"; \
fi; \
sleep $(2); \
done
endef
# Deploy everything: build, restart, verify health
# NOTE: No explicit service list — starts ALL services in the compose file.
# On macOS the proxy's nginx.local.conf references upstreams by Docker DNS name
# (e.g. "mcp"), and nginx crashes at startup if the upstream container doesn't
# exist on the network. Starting everything avoids this.
# The dedicated bridge network the container-image engine attaches artifact
# steps to. It must be created explicitly: no bnk-forge service joins it (they
# can't — under host networking a service cannot also join a bridge network),
# and `docker compose up` does NOT create a declared network that no service
# references. Without this, every artifact step dies with "network not found".
ARTIFACT_NETWORK ?= bnk-forge-artifacts
# Docker's auto-assigned pool (172.17.0.0/16+, typically landing on
# 172.18.0.0/16) can collide with a host's VPN/management routes, cutting off
# connectivity mid-deploy. No single hardcoded subnet is safe everywhere (a
# fixed 10.200.0.0/24 default also collided on a field site), so the subnet
# is resolved by scripts/artifact_network.sh: explicit ARTIFACT_NETWORK_SUBNET
# (or "auto" to defer to Docker's default-address-pools) wins; otherwise it
# auto-detects a free subnet from ARTIFACT_NETWORK_SUBNET_CANDIDATES against
# the host's routes and existing docker networks. Both vars may also be set
# in a .env file in the repo root. See issue #422.
ARTIFACT_NETWORK_SUBNET ?=
ARTIFACT_NETWORK_SUBNET_CANDIDATES ?=
ensure-artifact-network:
@export ARTIFACT_NETWORK="$(ARTIFACT_NETWORK)"; \
if [ -n "$(ARTIFACT_NETWORK_SUBNET)" ]; then export ARTIFACT_NETWORK_SUBNET="$(ARTIFACT_NETWORK_SUBNET)"; fi; \
if [ -n "$(ARTIFACT_NETWORK_SUBNET_CANDIDATES)" ]; then export ARTIFACT_NETWORK_SUBNET_CANDIDATES="$(ARTIFACT_NETWORK_SUBNET_CANDIDATES)"; fi; \
bash scripts/artifact_network.sh ensure
deploy: build ensure-artifact-network
@echo ""
@echo "=== Restarting containers ==="
$(COMPOSE) up -d --force-recreate
@echo ""
$(call wait-healthy,12,5)
@echo ""
@$(COMPOSE) ps
@echo ""
date
@echo ""
@echo "========================================="
@echo " Deploy complete"
@echo ""
ifeq ($(UNAME_S),Darwin)
@echo " Open: https://localhost"
@echo " Login: admin (password: DEFAULT_ADMIN_PASSWORD if set, else the generated one at /app/keys/initial_admin_password; change on first login)"
endif
@echo " Recommended next step: make mcp-readiness"
@echo "========================================="
# Deploy just backend (API + workers + beat) — most common for backend code changes
deploy-backend: build-backend build-worker ensure-artifact-network
@echo ""
@echo "=== Restarting backend containers ==="
$(COMPOSE) up -d --force-recreate backend celery-worker celery-worker-2 celery-beat
@echo ""
date
@echo ""
$(call wait-healthy,6,5)
# Deploy just frontend
deploy-frontend: build-frontend
@echo ""
@echo "=== Restarting frontend ==="
$(COMPOSE) up -d --force-recreate frontend
# Run strict safe upgrade flow (preflight + upgrade + verification)
upgrade-safe:
@echo ""
@echo "=== Running safe upgrade flow (upgrade.sh --local) ==="
@echo " Includes git/disk preflight and strict verification gates."
@./upgrade.sh --local
# Test upgrade policy guards (runs in isolated temp repo)
test-upgrade:
@echo ""
@echo "=== Testing upgrade.sh policy guards ==="
@./scripts/test-upgrade-policy.sh
# ShellCheck lint for all shell scripts
shellcheck:
@echo ""
@echo "=== ShellCheck: linting shell scripts ==="
@# bonnyr-f5 #182: drive from git ls-files so the WHOLE corpus is gated
@# (the hardcoded globs missed 14 tracked scripts incl. dist/install.sh).
@# bonnyr-f5 #182 r2: include the (extensionless) git hooks, and fail on an
@# EMPTY list -- `xargs shellcheck` with no files exits 0 on BSD (blind).
@files="$$(git ls-files '*.sh' .githooks/pre-commit .githooks/pre-push 2>/dev/null)"; \
n=$$(printf '%s\n' "$$files" | grep -c .); \
[ "$$n" -ge 1 ] || { echo "::error::shellcheck found no files to lint"; exit 1; }; \
printf '%s\n' "$$files" | xargs shellcheck --severity=warning
# ── CI-parity gates (bonnyr-f5 #182 r3, Major) ──────────────────────────────
# The four gates ci.yml added were not runnable locally: `make pre-push` ran
# none of them and `make shellcheck` had no dependents, yet ci.yml's header
# claims `make pre-push` == CI. #166: "a local gate that does not run the CI
# command is not a gate." These targets ARE the CI command (ci.yml calls the
# same `make` targets / same scripts), and `pre-push` now depends on `ci-gates`.
.PHONY: ci-gates secret-scan commit-lint script-selftests
# NOTE: `version-check` is defined once, in the "Version-artifact consistency"
# section below (near quick-check, which depends on it). A duplicate recipe used
# to sit here; `make` silently discarded one and warned "overriding commands for
# target" on every invocation, so any future divergence between the two copies
# would have been invisible (bonnyr-f5 #193 minor). ci-gates references the single
# surviving target by name.
# gitleaks range-aware secret scan + assertion backstop (single source of truth,
# shared with ci.yml's secret-scan job and the scheduled baseline workflow).
# Honours RANGE from the environment; unset => scan since the upstream merge-base.
secret-scan:
@echo ""
@echo "=== Secret scan (gitleaks) ==="
@bash scripts/secret-scan.sh
# Commit-message marker enforcement (shared with ci.yml's commit-lint job and
# .githooks/pre-push). Honours RANGE; unset => @{upstream}..HEAD.
commit-lint:
@echo ""
@echo "=== Commit message marker lint ==="
@bash scripts/lint-commit-markers.sh
# The paired self-test harnesses ci.yml's script-selftests job runs.
# ci.yml's compute step has FOUR anti-vacuity assertions and this target must
# mirror ALL of them, or a broken harness passes locally while CI goes red
# (bonnyr-f5 #182 r4/r5, Major-3: a local gate that diverges from the CI command
# is not a gate). The four (in ci.yml order):
# 1. non-zero exit -> the harness itself errored
# 2. a "FAIL:" line (rc still 0) -> an assertion failed but exit was swallowed
# 3. NO "PASS:" line -> the guard was silenced / renamed: green with
# zero assertions actually run
# 4. NO "=== END SELF-TEST ===" -> the harness exited early (deleted END marker
# or an early `exit 0`) with assertions unrun
# r5 landed 3+4 here; r4 had only 1+2, so the "silenced guard" and "early exit"
# harness-break modes were CI-red but `make`-GREEN.
script-selftests:
@echo ""
@echo "=== Script self-tests ==="
@set +e; out="$$(SELF_TEST=1 bash scripts/compute_version_bump.sh 2>&1)"; rc=$$?; \
echo "$$out"; \
if [ "$$rc" -ne 0 ]; then echo "::error::compute_version_bump self-test exited $$rc"; exit "$$rc"; fi; \
if printf '%s\n' "$$out" | grep -qE '(^|[[:space:]])FAIL:'; then \
echo "::error::compute_version_bump self-test reported FAIL: but exited 0"; exit 1; \
fi; \
if ! printf '%s\n' "$$out" | grep -qE '(^|[[:space:]])PASS:'; then \
echo "::error::self-test produced no PASS lines -- the harness did not run"; exit 1; \
fi; \
if ! printf '%s\n' "$$out" | grep -qE '=== END SELF-TEST ==='; then \
echo "::error::self-test did not reach its END marker -- it exited early with assertions unrun"; exit 1; \
fi
@# extract-breaking-changes.sh self-test, run UNCONDITIONALLY with the same
@# anti-vacuity assertions as compute's (ok lines + END marker). Do NOT gate on
@# `grep -- '--self-test' <the script>`: the code under test must not decide
@# whether it is tested — deleting the flag would silence ~28 assertions with
@# the gate staying green (bonnyr-f5 #193 M6).
@set +e; out="$$(bash scripts/extract-breaking-changes.sh --self-test 2>&1)"; rc=$$?; \
echo "$$out"; \
if [ "$$rc" -ne 0 ]; then echo "::error::extract-breaking-changes self-test exited $$rc"; exit "$$rc"; fi; \
if printf '%s\n' "$$out" | grep -qE '(^|[[:space:]])FAIL:'; then \
echo "::error::extract self-test reported FAIL: but exited 0"; exit 1; \
fi; \
if ! printf '%s\n' "$$out" | grep -qE '(^|[[:space:]])ok:'; then \
echo "::error::extract self-test produced no ok: lines -- the harness did not run"; exit 1; \
fi; \
if ! printf '%s\n' "$$out" | grep -qE '=== END SELF-TEST ==='; then \
echo "::error::extract self-test did not reach its END marker -- it exited early with assertions unrun"; exit 1; \
fi
@# B5 + M5 + M4: enumerate and run EVERY scripts/tests/*.test.sh from the
@# filesystem, applying the SAME anti-vacuity discipline as the two inline
@# self-tests above (bonnyr-f5 #193 r4 M-4: the old loop checked ONLY a non-empty
@# enumeration and each file's exit 0, so a test gutted to `exit 0` passed and
@# deleting 7 of 8 files left n=1 and stayed green). Each file must now:
@# 1. EXIT 0;
@# 2. emit at least one PASS line (the harness actually ran assertions);
@# 3. emit NO `FAIL` line (a failure whose exit was swallowed);
@# 4. reach its `ALL PASS` terminal (a gutted/early-exiting file never prints it).
@# PLUS a count floor DERIVED from the tests present, cross-checked against git's
@# tracked set: a test file deleted in the working tree makes the on-disk count fall
@# below the tracked count and is caught, instead of silently lowering a literal
@# floor. ci.yml's script-selftests job runs the SAME enumeration so local == CI.
@set -e; \
tests="$$(ls scripts/tests/*.test.sh 2>/dev/null || true)"; \
n=$$(printf '%s\n' "$$tests" | grep -c . || true); \
tracked=$$(git ls-files 'scripts/tests/*.test.sh' 2>/dev/null | grep -c . || true); \
if [ "$$n" -lt 1 ]; then echo "::error::no scripts/tests/*.test.sh found -- the self-test enumeration is empty"; exit 1; fi; \
if [ "$$tracked" -gt 0 ] && [ "$$n" -lt "$$tracked" ]; then \
echo "::error::self-test enumeration found $$n file(s) on disk but git tracks $$tracked -- a *.test.sh was removed from the working tree; refusing to run a shrunken suite"; exit 1; \
fi; \
echo " running $$n filesystem self-test(s) (git tracks $$tracked):"; \
for t in $$tests; do \
echo "--- $$t ---"; \
set +e; out="$$(bash "$$t" 2>&1)"; rc=$$?; set -e; \
printf '%s\n' "$$out"; \
if [ "$$rc" -ne 0 ]; then echo "::error::$$t exited $$rc"; exit 1; fi; \
if printf '%s\n' "$$out" | grep -qE '^FAIL'; then echo "::error::$$t printed a FAIL line but exited 0 -- a swallowed assertion failure"; exit 1; fi; \
if ! printf '%s\n' "$$out" | grep -qE '^PASS'; then echo "::error::$$t produced no PASS line -- the harness did not run (gutted to a no-op?)"; exit 1; fi; \
if ! printf '%s\n' "$$out" | grep -q 'ALL PASS'; then echo "::error::$$t did not reach its 'ALL PASS' terminal marker -- it exited early with assertions unrun"; exit 1; fi; \
done
# Mirror of ci.yml's "P1 · Artifact Network Self-Test" job (bonnyr-f5 #193 minor:
# `make pre-push` ≡ CI was false — this job had no local target). Pure-logic, no
# Docker/network.
artifact-network-selftest:
@echo ""
@echo "=== Artifact network self-test (mirror of ci.yml artifact-network-self-test) ==="
@bash scripts/artifact_network.sh --self-test
# Aggregate: every CI gate that is not already covered by quick-check/tests.
# bonnyr-f5 #193 minor (`make pre-push` ≡ CI): artifact-network-selftest is wired
# in here, and ci.yml's "migration-collision-check" job runs `make check-migrations`
# — already pulled in by quick-check (a pre-push prerequisite) — so both formerly
# unmirrored CI jobs are now reachable from `make pre-push`.
ci-gates: shellcheck version-check commit-lint script-selftests secret-scan artifact-network-selftest
@echo ""
@echo "=== CI-parity gates passed ==="
# Convenience: start/stop/restart all (platform-aware)
up: ensure-artifact-network
$(COMPOSE) up -d
down:
$(COMPOSE) down
restart:
$(COMPOSE) restart
logs:
$(COMPOSE) logs -f --tail 50
# ─── Default: run all tests ─────────────────────────────────────────────────
test: lint test-backend test-frontend test-proxy test-operator test-db
@echo ""
@echo "========================================="
@echo " All tests passed"
@echo "========================================="
# ─── Individual test suites ──────────────────────────────────────────────────
test-backend: $(BACKEND_PREREQ)
@echo ""
@echo "=== Backend Tests ==="
@cd backend && $(BACKEND_VENV) \
python -m pytest tests/ --tb=short -q
test-frontend: $(FRONTEND_PREREQ)
@echo ""
@echo "=== Frontend Tests ==="
@cd frontend-v2 && npm test -- --run
test-proxy: SUITE = proxy
test-proxy: $(BACKEND_PREREQ)
@echo ""
@echo "=== Proxy Config Tests ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/test_proxy_config.py --tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
test-operator: $(OPERATOR_PREREQ)
@echo ""
@echo "=== Operator Tests ==="
@cd bnk-operator && $(OPERATOR_VENV) \
python -m pytest tests/ --tb=short -q
test-mcp:
@echo ""
@echo "=== MCP Server Tests ==="
@cd mcp-server && \
if [ -d ".venv" ]; then source .venv/bin/activate; fi && \
python -m pytest tests/ --tb=short -q
smoke-mcp-live:
@echo ""
@echo "=== MCP Live Smoke Validation ==="
@echo " NOTE: ping/tools-list reachability != runtime readiness; tool calls require valid MCP backend credentials."
@echo " Configure MCP_SERVICE_PASSWORD (compose maps it to the container's BNK_FORGE_PASSWORD and the backend's MCP_SERVICE_PASSWORD) — the dedicated MCP service account, never the admin login (#187). MCP_USERNAME is not read; do not set it."
@python3 scripts/mcp_live_smoke.py --mcp-url "$${MCP_SMOKE_URL:-http://localhost:8081/mcp}" $${MCP_SMOKE_INSECURE_TLS:+--insecure-tls}
mcp-readiness:
@echo ""
@echo "=== MCP Readiness Check (Liveness + Runtime) ==="
@echo " Layer 1: container liveness (protocol ping healthcheck)"
@state=$$(docker inspect -f '{{.State.Status}}' bnk-forge-mcp 2>/dev/null) || (echo " ERROR: bnk-forge-mcp container not found. Start stack first (make deploy)." && exit 1); \
health=$$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' bnk-forge-mcp 2>/dev/null); \
echo " bnk-forge-mcp: state=$$state health=$$health"; \
if [ "$$state" != "running" ]; then \
echo " ERROR: MCP container is not running."; \
exit 1; \
fi; \
if [ "$$health" != "none" ] && [ "$$health" != "healthy" ]; then \
echo " ERROR: MCP protocol liveness healthcheck is not healthy."; \
exit 1; \
fi
@echo " Layer 2: runtime readiness (governed tool calls via smoke-mcp-live)"
@$(MAKE) --no-print-directory smoke-mcp-live
mcp-recreate:
@echo ""
@echo "=== Recreate MCP service ==="
@echo " Use after MCP_SERVICE_PASSWORD changes so MCP picks up new credentials."
@$(COMPOSE) up -d --force-recreate --no-deps mcp
@$(COMPOSE) ps mcp
test-db: SUITE = db-migration
test-db: $(BACKEND_PREREQ)
@echo ""
@echo "=== DB Migration Tests ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/test_migrations.py --tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
test-backend-unit: SUITE = unit
test-backend-unit: $(BACKEND_PREREQ)
@echo ""
@echo "=== Backend Unit Tests ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/unit/ --tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
test-backend-component: SUITE = component
test-backend-component: $(BACKEND_PREREQ)
@echo ""
@echo "=== Backend Component Tests ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/component/ --tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
test-contracts: SUITE = contract
test-contracts: $(BACKEND_PREREQ)
@echo ""
@echo "=== Golden Contract Tests ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/contract/ -v --tb=short $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
# Exact complement of test-integration-full: together they cover every test in
# tests/integration/. The selector is spelled out rather than inherited from
# pyproject's addopts (-m 'not full') on purpose -- that implicit coupling is
# what let the two targets stop being complements without anyone noticing
# (#130). Change one selector, change the other.
test-integration: SUITE = integration
test-integration: $(BACKEND_PREREQ)
@echo ""
@echo "=== Integration Tests (non-full marker set) ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/integration/ -m 'not full' --tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
# SUITE is integration-full, not integration: the artifact filenames are derived
# from it, and CI now runs both targets in one job.
test-integration-full: SUITE = integration-full
test-integration-full: $(BACKEND_PREREQ)
@echo ""
@echo "=== Full-Mode Integration Tests (requires running Docker stack) ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/integration/ -m full --tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
test-e2e: test-e2e-tier1
test-e2e-tier1:
@echo ""
@echo "=== E2E Tier 1 Tests (requires running stack, ~5 min) ==="
@cd tests/e2e && npx playwright test
test-e2e-tier2:
@echo ""
@echo "=== E2E Tier 2 Tests (requires running stack + AWS creds, ~30-60 min) ==="
@cd tests/e2e && E2E_TIER=2 npx playwright test tests/tier2/
# ─── Linting ─────────────────────────────────────────────────────────────────
lint: lint-backend lint-frontend
@echo ""
@echo "=== All linting passed ==="
lint-backend: $(BACKEND_PREREQ)
@echo ""
@echo "=== Backend Lint (ruff) ==="
@cd backend && $(BACKEND_VENV) \
python -m ruff check . --config pyproject.toml
lint-frontend: $(FRONTEND_PREREQ)
@echo ""
@echo "=== Frontend Lint (eslint) ==="
@cd frontend-v2 && set -o pipefail && npm run lint 2>&1 | tail -30
# ─── Coverage ────────────────────────────────────────────────────────────────
coverage: coverage-backend coverage-frontend
@echo ""
@echo "=== Coverage reports complete ==="
coverage-backend: | backend/.venv/bin/activate
@echo ""
@echo "=== Backend Coverage ==="
@cd backend && source .venv/bin/activate && \
python -m pytest tests/ --cov=. --cov-report=term-missing --cov-fail-under=40 --tb=short -q
coverage-frontend: | frontend-v2/node_modules
@echo ""
@echo "=== Frontend Coverage ==="
@cd frontend-v2 && npm run test:coverage
# ─── Pre-Push & Push (local checks → push → verify CI) ──────────────────────
typecheck-backend: $(BACKEND_PREREQ)
@echo ""
@echo "=== Backend Type Check (mypy core/ schemas/) ==="
@cd backend && $(BACKEND_VENV) \
python -m mypy core/ schemas/ --config-file pyproject.toml
typecheck-frontend: $(FRONTEND_PREREQ)
@echo ""
@echo "=== TypeScript Type Check (tsc --noEmit) ==="
@cd frontend-v2 && npx tsc --noEmit
test-backend-legacy: SUITE = legacy
test-backend-legacy: $(BACKEND_PREREQ)
@echo ""
@echo "=== Backend Legacy Tests (flat test_*.py files) ==="
@cd backend && $(BACKEND_VENV) \
$(PYTEST_BASE) tests/ \
--ignore=tests/unit \
--ignore=tests/component \
--ignore=tests/integration \
--ignore=tests/contract \
--ignore=tests/migrations \
--tb=short -q $(PYTEST_COV) $(PYTEST_COV_REPORT) $(PYTEST_JUNIT)
build-frontend-check: $(FRONTEND_PREREQ)
@echo ""
@echo "=== Frontend Build Check (npm run build) ==="
@cd frontend-v2 && npm run build > /dev/null 2>&1 && echo " Build succeeded ✓"
# ── Migration chain validator ────────────────────────────────────────────────
# Pure AST parsing — no DB, no venv, no alembic import. Runs in <1s.
check-migrations:
@echo "=== Migration Chain Validator ==="
@python3 scripts/check-migrations.py
# ── Version-artifact consistency ─────────────────────────────────────────────
# Mirror of CI's "P1 · Version Consistency" job. ci.yml promises `make pre-push`
# ≡ CI, so the gate must be reachable from the documented local target or drift
# is undetectable until the release job dies (bonnyr-f5 #180 r5, F3). Pulled in
# by quick-check (a pre-push prerequisite).
version-check:
@echo ""
@echo "=== Version Artifact Consistency (Helm tag/appVersion, frontend, operator) ==="
@bash scripts/sync-version-artifacts.sh --check
# ── Quick check (~15s): lint + types + contracts ────────────────────────────
# Run before every commit. Catches most CI failures instantly.
quick-check: lint typecheck-backend openapi-types-check check-migrations version-check
@echo ""
@echo "========================================="
@echo " Quick check passed (~15s)"
@echo "========================================="
# ── Pre-push (~90s parallel): mirrors ALL CI jobs ───────────────────────────
# Run once before git push. Runs test suites in parallel for speed.
# Prerequisite: quick-check runs first (sequential), then the CI-parity gates
# (shellcheck / version-check / commit-lint / script-selftests / secret-scan --
# bonnyr-f5 #182 r3, so `make pre-push` genuinely == CI), then tests fan out.
pre-push: quick-check ci-gates
@echo ""
@echo "=== Running all test suites in parallel... ==="
@failed=""; \
( set -o pipefail; make test-backend-unit 2>&1 | tail -3 ) & pid1=$$!; \
( set -o pipefail; make test-backend-component 2>&1 | tail -3 ) & pid2=$$!; \
( set -o pipefail; make test-backend-legacy 2>&1 | tail -3 ) & pid3=$$!; \
( set -o pipefail; make test-frontend 2>&1 | tail -3 ) & pid4=$$!; \
( set -o pipefail; make test-operator 2>&1 | tail -3 ) & pid5=$$!; \
( set -o pipefail; make test-proxy 2>&1 | tail -3 ) & pid6=$$!; \
( set -o pipefail; make test-db 2>&1 | tail -3 ) & pid7=$$!; \
( set -o pipefail; make typecheck-frontend 2>&1 | tail -3 ) & pid8=$$!; \
( set -o pipefail; make test-contracts 2>&1 | tail -3 ) & pid9=$$!; \
wait $$pid1 || failed="$$failed backend-unit"; \
wait $$pid2 || failed="$$failed backend-component"; \
wait $$pid3 || failed="$$failed backend-legacy"; \
wait $$pid4 || failed="$$failed frontend"; \
wait $$pid5 || failed="$$failed operator"; \
wait $$pid6 || failed="$$failed proxy"; \
wait $$pid7 || failed="$$failed db-migrations"; \
wait $$pid8 || failed="$$failed tsc"; \
wait $$pid9 || failed="$$failed contracts"; \
if [ -n "$$failed" ]; then \
echo ""; \
echo "========================================="; \
echo " FAILED:$$failed"; \
echo "========================================="; \
exit 1; \
fi
@echo ""
@echo "========================================="
@echo " Pre-push checks passed — safe to push"
@echo "========================================="
push: pre-push
@echo ""
@echo "=== Pushing to origin ==="
@git push
@echo ""
@echo "=== Waiting for CI to start... ==="
@sleep 10
@echo "=== Watching CI (this may take a few minutes)... ==="
@gh run watch --exit-status && \
echo "" && \
echo "=========================================" && \
echo " CI GREEN — all checks passed" && \
echo "=========================================" || \
(echo "" && \
echo "=========================================" && \
echo " CI RED — check failures with: gh run view" && \
echo "=========================================" && \
exit 1)
# ─── Containerized Tests (no local venv / Node needed) ──────────────────────
# Run tests and linting inside Docker images — matches CI Python 3.11 / Node 20
# exactly, sidesteps local venv drift (e.g. Python 3.14 on macOS).
#
# Usage:
# make test-docker — run all containerized tests
# make test-backend-docker — backend pytest in python:3.11 container
# make test-frontend-docker — frontend vitest in node:20 container
# make test-operator-docker — operator pytest in python:3.12 container
# make lint-backend-docker — ruff in container
#
# Source is bind-mounted so edits take effect immediately (no rebuild).
BACKEND_TEST_IMAGE := bnk-forge-backend-test
OPERATOR_TEST_IMAGE := bnk-forge-operator-test
FRONTEND_RUN_IMAGE := node:20-slim
DOCKER_RUN_FLAGS := --rm -v $(CURDIR):/repo -w /repo
.PHONY: test-docker test-backend-docker test-frontend-docker test-operator-docker \
lint-backend-docker build-test-images
test-docker: lint-backend-docker test-backend-docker test-frontend-docker test-operator-docker
@echo ""
@echo "========================================="
@echo " All containerized tests passed"
@echo "========================================="
build-test-images: .stamp/backend-test-image .stamp/operator-test-image
# VERSION is a prerequisite because the image now COPYs it — without this a
# version bump leaves a stale test image reporting the old number.
.stamp/backend-test-image: backend/Dockerfile backend/requirements.txt backend/requirements-dev.txt VERSION
@mkdir -p .stamp
@echo ""
@echo "=== Building backend test image ($(BACKEND_TEST_IMAGE)) ==="
docker build --target test -f backend/Dockerfile -t $(BACKEND_TEST_IMAGE) .
@touch $@
.stamp/operator-test-image: bnk-operator/Dockerfile bnk-operator/requirements.txt bnk-operator/requirements-dev.txt
@mkdir -p .stamp
@echo ""
@echo "=== Building operator test image ($(OPERATOR_TEST_IMAGE)) ==="
docker build --target test -t $(OPERATOR_TEST_IMAGE) bnk-operator/
@touch $@
test-backend-docker: .stamp/backend-test-image
@echo ""
@echo "=== Backend Tests (docker) ==="
docker run $(DOCKER_RUN_FLAGS) -w /repo/backend $(BACKEND_TEST_IMAGE) \
python -m pytest tests/ --tb=short -q
lint-backend-docker: .stamp/backend-test-image
@echo ""
@echo "=== Backend Lint (ruff, docker) ==="
docker run $(DOCKER_RUN_FLAGS) -w /repo/backend $(BACKEND_TEST_IMAGE) \
python -m ruff check . --config pyproject.toml
test-operator-docker: .stamp/operator-test-image
@echo ""
@echo "=== Operator Tests (docker) ==="
docker run $(DOCKER_RUN_FLAGS) -w /repo/bnk-operator $(OPERATOR_TEST_IMAGE) \
python -m pytest tests/ --tb=short -q
# Frontend: use stock node image + named volume for node_modules cache.
# First run does npm ci (~30s), subsequent runs reuse the volume (~0s).
test-frontend-docker:
@echo ""
@echo "=== Frontend Tests (docker) ==="
docker run $(DOCKER_RUN_FLAGS) \
-v bnk-forge-fe-node_modules:/repo/frontend-v2/node_modules \
-w /repo/frontend-v2 $(FRONTEND_RUN_IMAGE) \
sh -c '[ -f node_modules/.package-lock.json ] || npm ci; npm test -- --run'
# ─── Dev Environment Setup ───────────────────────────────────────────────────
# Creates local Python virtualenvs and installs frontend deps needed by the
# lint/test targets. File targets are idempotent — re-running is a no-op once
# the stamps exist. Delete the target path to force a rebuild.
dev-setup: backend/.venv/bin/activate bnk-operator/.venv/bin/activate frontend-v2/node_modules
@echo ""
@echo "=== Dev environment ready ==="
@echo " backend/.venv ✓"
@echo " bnk-operator/.venv ✓"
@echo " frontend-v2/node_modules ✓"
backend/.venv/bin/activate: backend/requirements.txt backend/requirements-dev.txt
@echo ""
@echo "=== Creating backend/.venv ==="
@cd backend && (python3.11 -m venv .venv || python3 -m venv .venv) && \
.venv/bin/pip install --upgrade pip && \
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
@touch $@
bnk-operator/.venv/bin/activate: bnk-operator/requirements.txt bnk-operator/requirements-dev.txt
@echo ""
@echo "=== Creating bnk-operator/.venv ==="
@cd bnk-operator && python3 -m venv .venv && \
source .venv/bin/activate && \
pip install --upgrade pip && \
pip install -r requirements.txt -r requirements-dev.txt
@touch $@
frontend-v2/node_modules: frontend-v2/package.json frontend-v2/package-lock.json
@echo ""
@echo "=== Installing frontend-v2 dependencies ==="
@cd frontend-v2 && npm ci