Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
567 lines (507 loc) · 30 KB
/
Copy pathMakefile
File metadata and controls
567 lines (507 loc) · 30 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
# Infrastructure choices persist across every lifecycle command.
-include .ops.env
export CLOUD_AGENT_NETWORK PGADMIN_PUBLISH
# Exporting an undefined variable supplies an empty value, which would mask a
# profile selected in a distribution env file or .env before Compose reads it.
ifneq ($(origin COMPOSE_PROFILES),undefined)
export COMPOSE_PROFILES
endif
.PHONY: help format lint test migrate test-verbose test-cov test-db test-all test-external rag-ingest \
frontend-install frontend-lint frontend-type-check frontend-test frontend-check \
check-all all-with-frontend \
test-db-local test-db-local-reset test-db-local-clean test-db-local-down docs docs-gettext docs-translate \
docs-lang docs-verify setup-dev clean check all \
docker-volumes up down restart ps logs build smoke demo-previous \
demo demo-smoke demo-down demo-reset _require-demo
# Default target
.DEFAULT_GOAL := help
# Allow overriding uv run flags, e.g.:
# make lint UV_RUN="uv run --active"
UV_RUN ?= uv run
PYTHON_VERSION ?= 3.12
# Frontend directory
FRONTEND_DIR := apps/frontend
# Colors for terminal output
RESET := \033[0m
BOLD := \033[1m
GREEN := \033[32m
YELLOW := \033[33m
BLUE := \033[34m
help: ## Show this help message
@echo "$(BOLD)Available targets:$(RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(BLUE)%-15s$(RESET) %s\n", $$1, $$2}'
format: ## Format code with ruff (configured for Google style)
@echo "$(YELLOW)Running formatter...$(RESET)"
$(UV_RUN) ruff format .
$(UV_RUN) ruff check --fix --unsafe-fixes .
@echo "$(GREEN)OK Code formatted$(RESET)"
lint: ## Run linters (ruff format check, ruff lint, pydocstyle)
@echo "$(YELLOW)Running linters...$(RESET)"
$(UV_RUN) ruff format --check .
$(UV_RUN) ruff check --no-fix .
$(UV_RUN) pydocstyle --match-dir='^((?!(tests|\.venv|node_modules|apps/frontend|ops|docs)).)*$$'
@echo "$(GREEN)OK Linting passed$(RESET)"
test: ## Run unit/integration tests (exclude external and db-dependent)
@echo "$(YELLOW)Running tests (not external, not dbtest)...$(RESET)"
@# -n auto --dist loadfile: parallelize across cores at file granularity
@# (same isolation unit as CI's file-based shards); ~3x faster wall time.
$(UV_RUN) pytest -q -m "not external and not dbtest" -n auto --dist loadfile
@echo "$(GREEN)OK Tests passed$(RESET)"
test-verbose: ## Run tests with verbose output (exclude external and db-dependent)
$(UV_RUN) pytest -vv -m "not external and not dbtest"
test-cov: ## Run tests with coverage (exclude external and db-dependent)
$(UV_RUN) pytest -m "not external and not dbtest" --cov=. --cov-report=term-missing --cov-report=html
test-db: ## Run tests that require PostgreSQL (set TEST_DB_HOST and TEST_DB_PORT)
@echo "$(YELLOW)Running database-dependent tests...$(RESET)"
TEST_DB_REQUIRED=1 $(UV_RUN) python -m tests.fixtures.postgres prepare
TEST_DB_REQUIRED=1 $(UV_RUN) pytest -vv -m "dbtest"
@echo "$(GREEN)OK Database tests passed$(RESET)"
test-db-local: ## Database tests on a shared local PostgreSQL container (ARGS="tests/..." to narrow)
UV_RUN="$(UV_RUN)" ops/dev/test-db-local.sh run $(ARGS)
test-db-local-reset: ## Drop this worktree's local test databases (recreated on the next run)
UV_RUN="$(UV_RUN)" ops/dev/test-db-local.sh reset
test-db-local-clean: ## Drop local test databases whose worktree no longer exists
UV_RUN="$(UV_RUN)" ops/dev/test-db-local.sh clean
test-db-local-down: ## Remove the local test PostgreSQL container and all its databases
ops/dev/test-db-local.sh down
test-all: ## Run all tests except external (includes db-dependent)
@echo "$(YELLOW)Running all tests (not external)...$(RESET)"
$(UV_RUN) pytest -q -m "not external" -n auto --dist loadfile
@echo "$(GREEN)OK All tests passed$(RESET)"
test-external: ## Run the live external-provider tests (needs real keys/servers)
$(UV_RUN) pytest -m external -vv
docs: ## Build the publishable docs site (every language) into docs/build/html
@echo "$(YELLOW)Building developer docs...$(RESET)"
@# Same command and flags as the "Docs Build" CI job and as the published
@# site: warnings are errors, --keep-going reports all of them instead of
@# stopping at the first. conf.py emits each non-root language into
@# docs/build/html/<code>/ from this one invocation, so what this produces
@# is the whole published tree. DOCS_LANGUAGES="en:English" builds English
@# alone when you only care about the source language.
$(UV_RUN) sphinx-build -b html docs/developer docs/build/html -W --keep-going
@echo "$(GREEN)OK Docs built: docs/build/html/index.html$(RESET)"
DOCS_LANG ?= zh_CN
docs-verify: docs docs-gettext ## Build the site and check every translation still applies
$(UV_RUN) python ops/ci/check_docs_links.py
@echo "$(YELLOW)Checking translations against the English build...$(RESET)"
@# A translation never fails loudly: Sphinx falls back to English for any
@# string it cannot translate, so a stale catalog builds clean under -W and
@# reads as English. This is what notices.
$(UV_RUN) python ops/ci/check_docs_translations.py \
docs/build/html docs/developer/locale --gettext-dir docs/gettext
docs-gettext: ## Extract translatable strings from the docs into docs/gettext/
@echo "$(YELLOW)Extracting translatable strings...$(RESET)"
$(UV_RUN) sphinx-build -b gettext docs/developer docs/gettext -q
@echo "$(GREEN)OK Catalog templates: docs/gettext/$(RESET)"
docs-translate: docs-gettext ## Create or update catalogs for DOCS_LANG (default zh_CN)
@echo "$(YELLOW)Updating $(DOCS_LANG) catalogs...$(RESET)"
@# uvx rather than a pinned dev dependency: this runs only when someone is
@# actually translating, so it should not sit in every contributor's venv.
uvx sphinx-intl update -p docs/gettext -d docs/developer/locale -l $(DOCS_LANG)
@echo "$(GREEN)OK Edit docs/developer/locale/$(DOCS_LANG)/LC_MESSAGES/*.po$(RESET)"
docs-lang: ## Build one translated language into docs/build/html-$(DOCS_LANG)
@echo "$(YELLOW)Building docs in $(DOCS_LANG)...$(RESET)"
@# Untranslated strings fall back to English, so this builds a complete
@# site however little has been translated.
$(UV_RUN) sphinx-build -b html docs/developer docs/build/html-$(DOCS_LANG) \
-D language=$(DOCS_LANG) -W --keep-going
@echo "$(GREEN)OK Docs built: docs/build/html-$(DOCS_LANG)/index.html$(RESET)"
rag-ingest: ## Build the docs RAG index (real bge-m3; RAG_EMBEDDER=hash for offline)
@echo "$(YELLOW)Building docs RAG index...$(RESET)"
PYTHONPATH=apps/backend $(UV_RUN) python -m serving.rag.ingest --embedder $${RAG_EMBEDDER:-gateway}
@echo "$(GREEN)OK RAG index built$(RESET)"
check: lint test ## Run all checks (lint, test)
@echo "$(GREEN)OK All checks passed$(RESET)"
all: format check ## Format code and run all checks
setup-dev: ## Set up development environment
@echo "$(YELLOW)Setting up development environment...$(RESET)"
@# Create venv if it doesn't exist; fail fast if an existing venv uses a different Python minor.
@if [ -x .venv/bin/python ]; then \
current="$$(.venv/bin/python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"; \
if [ "$$current" != "$(PYTHON_VERSION)" ]; then \
echo "Existing .venv uses Python $$current, expected $(PYTHON_VERSION). Remove .venv or rerun with PYTHON_VERSION=$$current."; \
exit 1; \
fi; \
else \
uv venv -p $(PYTHON_VERSION); \
fi
@# Install package in editable mode
uv pip install -e .
@# Sync development dependencies from pyproject.toml
uv sync --group dev
$(UV_RUN) pre-commit install
@echo "$(GREEN)✓ Development environment ready$(RESET)"
clean: ## Clean build artifacts and cache
@echo "$(YELLOW)Cleaning up...$(RESET)"
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete
find . -type f -name "*.pyo" -delete
find . -type f -name ".coverage" -delete
rm -rf htmlcov/ .pytest_cache/ .mypy_cache/ .ruff_cache/
@echo "$(GREEN)OK Cleanup complete$(RESET)"
# Frontend targets
frontend-install: ## Install frontend dependencies
@echo "$(YELLOW)Installing frontend dependencies...$(RESET)"
cd $(FRONTEND_DIR) && npm ci
@echo "$(GREEN)OK Frontend dependencies installed$(RESET)"
frontend-lint: ## Run ESLint on frontend code
@echo "$(YELLOW)Running frontend linter...$(RESET)"
cd $(FRONTEND_DIR) && npm run lint
@echo "$(GREEN)OK Frontend linting passed$(RESET)"
frontend-type-check: ## Run TypeScript type checking
@echo "$(YELLOW)Running TypeScript type check...$(RESET)"
cd $(FRONTEND_DIR) && npm run type-check
@echo "$(GREEN)OK Type checking passed$(RESET)"
frontend-test: ## Run frontend tests
@echo "$(YELLOW)Running frontend tests...$(RESET)"
cd $(FRONTEND_DIR) && npm run test
@echo "$(GREEN)OK Frontend tests passed$(RESET)"
frontend-check: frontend-lint frontend-type-check frontend-test ## Run all configured frontend checks
@echo "$(GREEN)OK All configured frontend checks passed$(RESET)"
# Combined targets
check-all: lint test frontend-check ## Run all checks (backend + frontend)
@echo "$(GREEN)OK All checks passed (backend + frontend)$(RESET)"
all-with-frontend: format check-all ## Format and check everything (backend + frontend)
# ─── Docker / Production ─────────────────────────────────────────────────────
# A deployment's public identity — site name, links, CORS, and the console's
# build-time values — lives in its distribution overlay, because the upstream
# compose defaults name no deployment. These have to reach `make build` too,
# not just the deploy scripts: the console's identity is baked in as build
# args, so a rebuild without them ships an unbranded frontend. `.env` stays
# last so per-host overrides still win, and secrets stay only in `.env`.
# Which deployment's identity `make up` and `make build` compile in.
#
# Discovered by default, because the runbooks tell operators to run these by
# hand on the server (docs/developer/deployment.md, adding-models.md), and a
# rebuild that quietly dropped the identity would publish an unbranded console
# from a routine command. Discovery keeps that working with no change to any
# machine.
#
# A clone of this repository carries no deployment overlay (example/ is marked
# EXAMPLE_OVERLAY and never auto-selected), so discovery finds nothing and the
# result is neutral. Where an overlay is present, discovery announces it rather
# than staying silent, and `DISTRIBUTION=none` opts out:
#
# make up # discovers the overlay, and says so
# make up DISTRIBUTION=none # your own gateway, named after nobody
# make up DISTRIBUTION=<name> # pick one when several are present
#
# distributions/<name>/deploy/<file>.env -> <name>, deduplicated.
#
# An EXAMPLE_OVERLAY regular file marks a teaching artifact. Such overlays are
# available by name but never auto-selected as deployments. `test -f` matches
# the backend fallback's Path.is_file() check.
_ALL_DISTRIBUTION_DIRS := $(sort $(foreach f,$(wildcard distributions/*/deploy/*.env),$(word 2,$(subst /, ,$(f)))))
_EXAMPLE_DISTRIBUTION_DIRS := $(sort $(foreach d,$(_ALL_DISTRIBUTION_DIRS),\
$(if $(shell test -f 'distributions/$(d)/EXAMPLE_OVERLAY' && echo yes),$(d),)))
_DISTRIBUTION_DIRS := $(filter-out $(_EXAMPLE_DISTRIBUTION_DIRS),$(_ALL_DISTRIBUTION_DIRS))
ifeq ($(words $(_DISTRIBUTION_DIRS)),1)
DISTRIBUTION ?= $(_DISTRIBUTION_DIRS)
else ifeq ($(words $(_DISTRIBUTION_DIRS)),0)
DISTRIBUTION ?=
else
# Picking the alphabetically first of several would compile one deployment's
# identity into another's console, and say nothing while doing it.
DISTRIBUTION ?= $(error Several distributions carry deploy/*.env ($(_DISTRIBUTION_DIRS)). Name one: make $(MAKECMDGOALS) DISTRIBUTION=<name>, or DISTRIBUTION=none)
endif
# These are outputs of DISTRIBUTION selection, never independent inputs. The
# initial values also close the neutral/empty-selector branch against command-
# line injection.
override DISTRIBUTION_PATH :=
override DISTRIBUTION_ENV_FILES :=
override _IS_EXAMPLE :=
ifeq ($(DISTRIBUTION),none)
else ifneq ($(DISTRIBUTION),)
# DISTRIBUTION_PATH is derived state, not a second selector. In particular,
# `DISTRIBUTION=example DISTRIBUTION_PATH=distributions/<deployment>` must not
# combine the example lifecycle with a deployment's Compose/env files.
override DISTRIBUTION_PATH := $(wildcard distributions/$(DISTRIBUTION))
ifeq ($(DISTRIBUTION_PATH),)
$(error DISTRIBUTION=$(DISTRIBUTION) matches no distributions/$(DISTRIBUTION))
endif
override _DISTRIBUTION_ENV_PATHS := $(wildcard $(DISTRIBUTION_PATH)/deploy/*.env)
override DISTRIBUTION_ENV_FILES := $(patsubst %,--env-file %,$(_DISTRIBUTION_ENV_PATHS))
ifeq ($(DISTRIBUTION_ENV_FILES),)
$(error DISTRIBUTION=$(DISTRIBUTION) matches no $(DISTRIBUTION_PATH)/deploy/*.env)
endif
# Derive the internal branch from the selected overlay, not from dotenv values.
override _IS_EXAMPLE := $(shell test -f '$(DISTRIBUTION_PATH)/EXAMPLE_OVERLAY' && echo yes)
ifeq ($(_IS_EXAMPLE),)
$(info Using distribution '$(DISTRIBUTION)' — its identity is compiled into the console. DISTRIBUTION=none for a neutral stack.)
endif
endif
# Deploy scripts may append environment files for a deployment-specific
# environment, such as staging. These are inserted after the shared
# distribution files and before `.env`, preserving host-local overrides.
COMPOSE_EXTRA_ENV_FILES ?=
COMPOSE_EXTRA_ENV_ARGS := $(patsubst %,--env-file %,$(wildcard $(COMPOSE_EXTRA_ENV_FILES)))
# The cloud-agent runner overlay was removed at H4: agents run from their own
# repository and their own deployment, so this stack no longer has an opt-in
# that attaches `backend` to an agent network.
COMPOSE_FILE_ARGS := -f deploy/docker/docker-compose.yml
DISTRIBUTION_COMPOSE_FILE := $(if $(DISTRIBUTION_PATH),$(wildcard $(DISTRIBUTION_PATH)/deploy/docker-compose.yml),)
ifneq ($(DISTRIBUTION_COMPOSE_FILE),)
COMPOSE_FILE_ARGS += -f $(DISTRIBUTION_COMPOSE_FILE)
endif
# Standalone cloud agent on the same host (hybridInference-cloud-agent). Opting
# in attaches the console to that stack's network so the `/agents` rewrites
# from #1206 can resolve `web` and `control-plane`; without it they resolve
# nothing and the console answers 500 for a page it is configured to serve.
# Off by default because the network is `external` — naming one that does not
# exist fails every compose command on every other deployment.
ifeq ($(CLOUD_AGENT_NETWORK),1)
COMPOSE_FILE_ARGS += -f deploy/docker/docker-compose.cloud-agent.yml
endif
# pgAdmin publishes no host port unless asked: the console reaches it on its
# own network, behind the admin session check, and a host port skips that
# check. PGADMIN_PUBLISH=1 publishes it on 127.0.0.1, port PGADMIN_PORT (5050
# unless set), for an SSH tunnel. Pass it with COMPOSE_PROFILES=admin on every
# command, or the next one recreates pgAdmin without the port.
ifeq ($(PGADMIN_PUBLISH),1)
COMPOSE_FILE_ARGS += -f deploy/docker/docker-compose.pgadmin-port.yml
endif
# A host-local `.env` remains last whenever it exists, preserving its override
# precedence. Public runnable examples deliberately need no secret file, so do
# not hand Compose a path that is absent on a fresh clone.
LOCAL_ENV_ARGS := $(if $(wildcard .env),--env-file .env,)
# The example is deterministic even in an operator checkout that already has a
# deployment .env. Shell variables still outrank every --env-file in Compose,
# which is the explicit escape hatch documented for a real upstream.
ifeq ($(_IS_EXAMPLE),yes)
LOCAL_ENV_ARGS :=
endif
# Each deployment's lifecycle record: the images `make deploy` or `make
# rollback` last started (images.env), the releases a rollback chooses between
# and, by default, its database dumps. One directory per deployment -- the
# selected distribution, and the Compose project when the shell names one -- so
# the example, another overlay or a second project in this checkout never
# starts this one's images. While images.env exists, every Compose command of
# this deployment starts those images instead of building the checkout; `make
# build` refuses until the file is removed (docs/developer/deployment.md).
OPS_STATE := .ops/$(if $(DISTRIBUTION_PATH),$(DISTRIBUTION),none)$(if $(COMPOSE_PROJECT_NAME),/$(COMPOSE_PROJECT_NAME),)
ARTIFACT_ENV_FILE := $(wildcard $(OPS_STATE)/images.env)
ARTIFACT_ENV_ARGS := $(if $(ARTIFACT_ENV_FILE),--env-file $(ARTIFACT_ENV_FILE),)
ifneq ($(ARTIFACT_ENV_FILE),)
COMPOSE_FILE_ARGS += -f deploy/docker/docker-compose.artifacts.yml
endif
# A teaching overlay's database-less Stage 1 reads its documents as JSON
# environment seeds, taken from the selected overlay's own files.
_json_env = $(if $(wildcard $(1)),$(shell python3 -c 'import json, sys; print(json.dumps(json.load(open(sys.argv[1]))))' '$(1)'),)
ifeq ($(_IS_EXAMPLE),yes)
export MODEL_CATALOG := $(call _json_env,$(DISTRIBUTION_PATH)/config/models.json)
export ROUTING_POLICY := $(call _json_env,$(DISTRIBUTION_PATH)/config/routing.json)
export SITE_CONFIGURATION := $(call _json_env,$(DISTRIBUTION_PATH)/distribution.json)
endif
COMPOSE := docker compose $(COMPOSE_FILE_ARGS) $(DISTRIBUTION_ENV_FILES) $(COMPOSE_EXTRA_ENV_ARGS) $(LOCAL_ENV_ARGS)$(if $(ARTIFACT_ENV_ARGS), $(ARTIFACT_ENV_ARGS),)
# The full local demo is a third Compose layer on top of the runnable example.
# Keep COMPOSE above untouched: `make up DISTRIBUTION=example` is the Stage 1
# contract, while only the explicit demo targets opt into Postgres + frontend.
DEMO_COMPOSE_FILE := $(if $(DISTRIBUTION_PATH),$(wildcard $(DISTRIBUTION_PATH)/deploy/docker-compose.demo.yml),)
DEMO_COMPOSE_FILE_ARG := $(if $(DEMO_COMPOSE_FILE),-f $(DEMO_COMPOSE_FILE),)
# The teaching lifecycle never inherits optional production profiles from a
# caller's shell. `env` also keeps this usable as full_smoke's argv-based
# recreate command, where a bare `NAME=value` token would not be executable.
DEMO_COMPOSE := env COMPOSE_PROFILES= docker compose $(COMPOSE_FILE_ARGS) $(DEMO_COMPOSE_FILE_ARG) $(DISTRIBUTION_ENV_FILES) $(COMPOSE_EXTRA_ENV_ARGS) $(LOCAL_ENV_ARGS)$(if $(ARTIFACT_ENV_ARGS), $(ARTIFACT_ENV_ARGS),)
DOCKER_VOLUMES := hybridinference_postgres_data
# The schema migrations, run with the backend's own service definition so they
# use the very image (and DB_* settings) the backend is about to start with.
# A one-off container, never a dependency of `backend`: restoring the previous
# image must not run that image's migrations against a newer schema.
#
# --no-deps, with the database started first by MIGRATE_DB: without it
# `compose run` brings the backend's dependencies up itself, recreating
# Postgres whenever its configuration changed, under the backend still
# serving and before a migration that may then fail. MIGRATE_DB starts
# Postgres only when it is not running, never recreates it, and waits until it
# is healthy; the `up -d` after a migration recreates it if it must.
MIGRATE_DB := $(COMPOSE) up -d --no-recreate --wait postgres
MIGRATE := $(COMPOSE) run --rm --no-deps backend python -m migrations upgrade
DEMO_MIGRATE := $(DEMO_COMPOSE) run --rm --no-deps backend python -m migrations upgrade
# Upgrading from a release that read its site policy, branding and alert rules
# from files: LEGACY_MANIFEST names that release's manifest as the backend
# container sees it (distributions/<name>/distribution.yaml, or the deployment's
# DISTRIBUTION_CONFIG_PATH), and the image about to start imports it after the
# migrations, before it serves. Settings already stored are kept, so a repeat
# changes nothing. LEGACY_IMPORT_OPTIONS passes options on to import-legacy
# (--include-dark, --alerts=<file>), and runs it even without a manifest.
LEGACY_MANIFEST ?=
LEGACY_IMPORT_OPTIONS ?=
LEGACY_IMPORT := $(if $(LEGACY_MANIFEST)$(LEGACY_IMPORT_OPTIONS),$(COMPOSE) run --rm --no-deps backend python -m serving.config.manage import-legacy$(if $(LEGACY_IMPORT_OPTIONS), $(LEGACY_IMPORT_OPTIONS))$(if $(LEGACY_MANIFEST), "$(LEGACY_MANIFEST)"),)
docker-volumes: ## Create external Docker volumes required by production compose
@for volume in $(DOCKER_VOLUMES); do \
if ! docker volume inspect "$$volume" >/dev/null 2>&1; then \
echo "$(YELLOW)Creating Docker volume $$volume...$(RESET)"; \
docker volume create "$$volume" >/dev/null; \
fi; \
done
# The runnable tutorial proves the backend routing chain, not the production
# frontend/database stack. Start its fake to healthy first, then use --no-deps
# so backend's production Postgres dependency stays stopped while DB_ENABLED is
# false. The production branch retains its external-volume prerequisite.
ifeq ($(_IS_EXAMPLE),yes)
up: ## Start all services
$(COMPOSE) up -d --wait example-provider
$(COMPOSE) up -d --no-deps backend
else
up: docker-volumes ## Start all services, after applying the schema migrations
$(MIGRATE_DB)
$(MIGRATE)
$(LEGACY_IMPORT)
$(COMPOSE) up -d
endif
migrate: docker-volumes ## Apply the schema migrations with the backend image (a deploy runs this first)
$(MIGRATE_DB)
$(MIGRATE)
# The port has one source of truth: the distribution's own env file, which is
# also the file Compose reads to publish it. Deriving the smoke URL from the
# same place means editing the distribution — the thing this repository teaches
# people to do — cannot leave `make smoke` probing a port nobody published. A
# shell override still outranks both, and Compose honours it too.
_DIST_ENV_FILES := $(if $(DISTRIBUTION_PATH),$(wildcard $(DISTRIBUTION_PATH)/deploy/*.env),)
_DIST_BACKEND_PORT := $(if $(_DIST_ENV_FILES),$(shell sed -n 's/^BACKEND_PORT=//p' $(_DIST_ENV_FILES) | tail -n 1),)
_DIST_FRONTEND_PORT := $(if $(_DIST_ENV_FILES),$(shell sed -n 's/^FRONTEND_PORT=//p' $(_DIST_ENV_FILES) | tail -n 1),)
_DIST_DEMO_PROVIDER_SERVICE := $(if $(_DIST_ENV_FILES),$(shell sed -n 's/^DEMO_PROVIDER_SERVICE=//p' $(_DIST_ENV_FILES) | tail -n 1),)
BACKEND_PORT ?= $(if $(_DIST_BACKEND_PORT),$(_DIST_BACKEND_PORT),8080)
FRONTEND_PORT ?= $(if $(_DIST_FRONTEND_PORT),$(_DIST_FRONTEND_PORT),3001)
DEMO_PROVIDER_SERVICE ?= $(_DIST_DEMO_PROVIDER_SERVICE)
# An overlay may ship its own smoke check, as the example always does; `make
# smoke` runs it against the backend port its env file publishes. Without one,
# the generic check below runs.
SMOKE_SCRIPT := $(if $(_IS_EXAMPLE),$(DISTRIBUTION_PATH)/smoke.py,$(if $(DISTRIBUTION_PATH),$(wildcard $(DISTRIBUTION_PATH)/smoke.py),))
ifneq ($(SMOKE_SCRIPT),)
SMOKE_BASE_URL ?= http://localhost:$(BACKEND_PORT)
else
# An empty URL asks gateway_ops to resolve the published endpoint from Compose,
# including .env and COMPOSE_EXTRA_ENV_FILES overrides.
SMOKE_BASE_URL ?=
endif
DEMO_BASE_URL ?= http://localhost:$(FRONTEND_PORT)
SMOKE_TIMEOUT ?= 120
SMOKE_PYTHON ?= python3
DEMO_SMOKE_SCRIPT := $(DISTRIBUTION_PATH)/full_smoke.py
DEMO_SMOKE_STATE_ARG := $(if $(DEMO_SMOKE_STATE_FILE),--write-reset-state "$(DEMO_SMOKE_STATE_FILE)",)
DEMO_SMOKE_EXPECT_STATE_ARG := $(if $(DEMO_SMOKE_EXPECT_STATE_FILE),--expect-existing-state "$(DEMO_SMOKE_EXPECT_STATE_FILE)",)
smoke: ## Check readiness and configuration; MODEL=<id> also checks inference
ifneq ($(SMOKE_SCRIPT),)
$(SMOKE_PYTHON) "$(SMOKE_SCRIPT)" --base-url "$(SMOKE_BASE_URL)" --timeout "$(SMOKE_TIMEOUT)"
else
$(SMOKE_PYTHON) -m ops.admin.gateway_ops smoke --base-url "$(SMOKE_BASE_URL)" --model "$(MODEL)" --compose $(COMPOSE)
endif
# Stage 2 deliberately reuses Stage 1's project and provider. --no-recreate
# pins that promise: Compose's divergence check can otherwise decide an
# unchanged service needs recreating (observed in CI whenever the provider
# image was built without cache) and silently restart it mid-tutorial. Only
# backend is recreated in place, picking up DB/auth.
_require-demo:
@test -n "$(DISTRIBUTION_PATH)" || (echo "The demo requires an explicit distribution"; exit 1)
@test -f "$(DEMO_COMPOSE_FILE)" || (echo "No demo Compose overlay for DISTRIBUTION=$(DISTRIBUTION)"; exit 1)
demo: _require-demo ## Upgrade the runnable example in place to the full local stack
$(DEMO_COMPOSE) up -d --no-recreate --wait $(DEMO_PROVIDER_SERVICE) postgres
$(DEMO_MIGRATE)
$(DEMO_COMPOSE) up -d --no-deps --force-recreate --wait backend
$(DEMO_COMPOSE) up -d --no-deps --no-recreate --wait frontend
# full_smoke keeps the original JWT and API key in memory while this Compose
# command recreates backend, then proves both still work afterwards. CI can
# opt into a private outside-checkout reset-proof file for a later reset check.
demo-smoke: _require-demo ## Verify the full demo through its frontend origin
@test -f "$(DEMO_SMOKE_SCRIPT)" || (echo "No full smoke script for DISTRIBUTION=$(DISTRIBUTION)"; exit 1)
$(SMOKE_PYTHON) "$(DEMO_SMOKE_SCRIPT)" --base-url "$(DEMO_BASE_URL)" --timeout "$(SMOKE_TIMEOUT)" $(DEMO_SMOKE_STATE_ARG) $(DEMO_SMOKE_EXPECT_STATE_ARG) --recreate-command $(DEMO_COMPOSE) up -d --no-deps --force-recreate --wait backend
# The restore drill, through the code `make rollback` runs. A failed deploy
# brings back the images that ran before it, on the schema the new image has
# just migrated, without running their migrations. Record
# PREVIOUS_BACKEND_IMAGE, with the console that is running, as the release to
# return to, in a record of the drill's own; gateway_ops then starts it as
# `make rollback` does, through the pinned-artifact overlay and the base
# file's pass-throughs, and runs the deploy acceptance on it. The full smoke
# follows, its recreate included. The deployment's own record is never
# touched, so `make demo` afterwards returns to the current image.
#
# An image from before the database-backed configuration reads its public site
# identity from the manifest its deployment still names, as a deployment keeps
# doing for its rollback window. The shell outranks every env file, and the
# base Compose file passes the selectors on.
DEMO_DRILL_STATE := $(OPS_STATE)/restore-drill
DEMO_PREVIOUS_COMPOSE := env COMPOSE_PROFILES= DISTRIBUTION_CONFIG_PATH=/app/$(DISTRIBUTION_PATH)/distribution.demo.json DISTRIBUTION_CONFIG_MODE=active docker compose $(COMPOSE_FILE_ARGS) $(DEMO_COMPOSE_FILE_ARG) $(DISTRIBUTION_ENV_FILES) $(COMPOSE_EXTRA_ENV_ARGS) $(LOCAL_ENV_ARGS)
DEMO_PREVIOUS_PINNED := $(DEMO_PREVIOUS_COMPOSE) -f deploy/docker/docker-compose.artifacts.yml --env-file $(DEMO_DRILL_STATE)/images.env
demo-previous: _require-demo ## Restore drill: roll back to PREVIOUS_BACKEND_IMAGE on the migrated demo database, smoked
@test -n "$(PREVIOUS_BACKEND_IMAGE)" || (echo "Set PREVIOUS_BACKEND_IMAGE to the image to restore"; exit 1)
mkdir -p -m 700 "$(DEMO_DRILL_STATE)"
backend="$$(docker image inspect --format '{{.Id}}' "$(PREVIOUS_BACKEND_IMAGE)")" && \
frontend="$$(docker container inspect --format '{{.Image}}' "$$($(DEMO_COMPOSE) ps -q frontend)")" && \
printf '{"backend": "%s", "frontend": "%s"}\n' "$$backend" "$$frontend" > "$(DEMO_DRILL_STATE)/previous.json"
$(SMOKE_PYTHON) -m ops.admin.gateway_ops rollback --state "$(DEMO_DRILL_STATE)" --base-url "$(DEMO_BASE_URL)" --timeout "$(SMOKE_TIMEOUT)" --compose $(DEMO_PREVIOUS_COMPOSE)
$(SMOKE_PYTHON) "$(DEMO_SMOKE_SCRIPT)" --base-url "$(DEMO_BASE_URL)" --timeout "$(SMOKE_TIMEOUT)" $(DEMO_SMOKE_EXPECT_STATE_ARG) --recreate-command $(DEMO_PREVIOUS_PINNED) up -d --no-deps --no-build --force-recreate --wait backend
demo-down: _require-demo ## Stop the full demo while preserving its database
$(DEMO_COMPOSE) down
demo-reset: _require-demo ## Stop the full demo and delete its local database
$(DEMO_COMPOSE) down --volumes --remove-orphans
down: ## Stop all services
$(COMPOSE) down
restart: ## Restart all services (or: make restart s=backend)
ifdef s
$(COMPOSE) restart $(s)
else
$(COMPOSE) restart
endif
ps: ## Show running services
$(COMPOSE) ps
logs: ## Tail logs (or: make logs s=backend)
ifdef s
$(COMPOSE) logs -f $(s)
else
$(COMPOSE) logs -f --tail=500
endif
# A build runs this checkout's code. On a deployment pinned to the images a
# deploy or rollback started, it would tag the build with a digest it does not
# have and start a different artifact than the record says; returning to source
# builds is a decision, made by removing the pin.
_require-source-build:
ifneq ($(ARTIFACT_ENV_FILE),)
@echo "This deployment runs the images pinned in $(ARTIFACT_ENV_FILE) by make deploy or make rollback." >&2
@echo "Deploy another release with make deploy, or remove that file to return to source builds." >&2
@exit 1
endif
# Keep rebuild semantics aligned with `up`: rebuilding the tutorial must not
# unexpectedly turn it into the production full stack.
ifeq ($(_IS_EXAMPLE),yes)
build: _require-source-build ## Rebuild images and restart (or: make build s=backend)
$(COMPOSE) up -d --wait example-provider
ifdef s
ifneq ($(filter-out backend example-provider,$(s)),)
$(error The runnable example can build only backend or example-provider)
endif
$(COMPOSE) up -d --build --no-deps $(s)
else
$(COMPOSE) up -d --build --no-deps backend
endif
else
build: _require-source-build docker-volumes
ifdef s
$(COMPOSE) build $(s)
$(MIGRATE_DB)
$(MIGRATE)
$(LEGACY_IMPORT)
$(COMPOSE) up -d $(s)
else
$(COMPOSE) build
$(MIGRATE_DB)
$(MIGRATE)
$(LEGACY_IMPORT)
$(COMPOSE) up -d
endif
endif
.PHONY: doctor deploy rollback backup restore-check _require-source-build
OPS_BASE_URL ?=
# Inside the deployment's private record, which neither Git nor a Docker build
# context includes: the dump holds every stored secret.
BACKUP ?= $(OPS_STATE)/gateway.dump
doctor: ## Diagnose services, readiness, schema and missing configuration
$(SMOKE_PYTHON) -m ops.admin.gateway_ops doctor --base-url "$(OPS_BASE_URL)" --compose $(COMPOSE)
deploy: docker-volumes ## Deploy pinned BACKEND_IMAGE and FRONTEND_IMAGE with acceptance and rollback
$(SMOKE_PYTHON) -m ops.admin.gateway_ops deploy --state "$(OPS_STATE)" --base-url "$(OPS_BASE_URL)" --model "$(MODEL)" --backend-image "$(BACKEND_IMAGE)" --frontend-image "$(FRONTEND_IMAGE)" --legacy-manifest "$(LEGACY_MANIFEST)" $(foreach option,$(LEGACY_IMPORT_OPTIONS),--legacy-import-option=$(option)) --compose $(COMPOSE)
rollback: ## Restore previously recorded images without running old migrations
$(SMOKE_PYTHON) -m ops.admin.gateway_ops rollback --state "$(OPS_STATE)" --base-url "$(OPS_BASE_URL)" --model "$(MODEL)" --compose $(COMPOSE)
backup: ## Write a private Postgres dump to BACKUP=<path>
$(SMOKE_PYTHON) -m ops.admin.gateway_ops backup --backup "$(BACKUP)" --compose $(COMPOSE)
restore-check: ## Restore BACKUP into a throwaway PostgreSQL server and migrate it to this release
$(SMOKE_PYTHON) -m ops.admin.gateway_ops restore-check --backup "$(BACKUP)" --compose $(COMPOSE)