diff --git a/AGENTS.md b/AGENTS.md index 3b1a1e496..84dbadfa4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,7 +124,7 @@ make extension-enable NAME=... # Enable an installed extension (restart requ make extension-disable NAME=... # Disable without uninstalling (restart required) make extension-remove NAME=... # Remove package and config entry (restart required) make dev # Start all services with hot-reload (Gateway + Frontend + Nginx) -make start # Start all services in production mode (local, optimized) +make start # Start all services in production mode (local, optimized); SKIP_FRONTEND_BUILD=1 reuses the last frontend build make stop # Stop all running services make up / down # Build/stop the production Docker stack (browser at localhost:2026) make docker-start / docker-stop / docker-logs # Docker development environment diff --git a/Makefile b/Makefile index b1c8fc9a7..687baf467 100644 --- a/Makefile +++ b/Makefile @@ -137,10 +137,12 @@ dev: @$(PYTHON) ./scripts/check.py @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --dev -# Start all services in production mode (with optimizations) +# Start all services in production mode (with optimizations). +# SKIP_FRONTEND_BUILD=1 reuses the existing frontend build instead of running +# `next build`; see scripts/serve.sh --skip-frontend-build. start: @$(PYTHON) ./scripts/check.py - @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod + @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod $(if $(filter 1,$(SKIP_FRONTEND_BUILD)),--skip-frontend-build) # Start all services in daemon mode (background) dev-daemon: @@ -150,7 +152,7 @@ dev-daemon: # Start prod services in daemon mode (background) start-daemon: @$(PYTHON) ./scripts/check.py - @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod --daemon + @$(RUN_WITH_GIT_BASH) ./scripts/serve.sh --prod --daemon $(if $(filter 1,$(SKIP_FRONTEND_BUILD)),--skip-frontend-build) # Start nginx alone in the foreground with the local dev config nginx: diff --git a/README.md b/README.md index 00dee1aeb..d688eff7d 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,11 @@ DeerFlow runs the agent runtime inside the Gateway API. Development mode enables | **Stop** | `./scripts/serve.sh --stop`
`make stop` | `./scripts/docker.sh stop`
`make docker-stop` | `./scripts/deploy.sh down`
`make down` | | **Restart** | `./scripts/serve.sh --restart [flags]` | `./scripts/docker.sh restart` | — | +`make start` and `make start-daemon` rebuild the frontend with `next build` on +every run. To reuse the last build instead, pass `SKIP_FRONTEND_BUILD=1` (or add +`--skip-frontend-build` when calling `./scripts/serve.sh --prod` directly). This +is opt-in: it fails fast when `frontend/.next` has no completed build. + Gateway owns `/api/langgraph/*` and translates those public LangGraph-compatible paths to its native `/api/*` routers behind nginx. #### LangGraph Studio (Optional) diff --git a/backend/tests/test_serve_frontend_skip_build.py b/backend/tests/test_serve_frontend_skip_build.py new file mode 100644 index 000000000..876ae63b2 --- /dev/null +++ b/backend/tests/test_serve_frontend_skip_build.py @@ -0,0 +1,55 @@ +"""Regression coverage for the opt-in --skip-frontend-build startup flag.""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +SERVE_SH = REPO_ROOT / "scripts" / "serve.sh" +ROOT_MAKEFILE = REPO_ROOT / "Makefile" + + +def _make_recipe(target: str) -> str: + makefile = ROOT_MAKEFILE.read_text(encoding="utf-8") + match = re.search(rf"^{target}:\n((?:\t.*\n?)+)", makefile, re.M) + assert match, f"target {target!r} not found in root Makefile" + return match.group(1) + + +def test_serve_script_parses_skip_frontend_build_flag() -> None: + serve = SERVE_SH.read_text(encoding="utf-8") + + assert "SKIP_FRONTEND_BUILD=false" in serve + assert "--skip-frontend-build) SKIP_FRONTEND_BUILD=true ;;" in serve + + +def test_prod_default_still_builds_via_preview() -> None: + serve = SERVE_SH.read_text(encoding="utf-8") + + # Verify the control-flow: skip flag -> `run start`, otherwise -> `run preview`. + assert re.search( + r"elif\s+\$SKIP_FRONTEND_BUILD;\s+then.*?FRONTEND_CMD=.*?run start.*?\nelse\n\s*FRONTEND_CMD=.*?run preview", + serve, + re.S, + ), "expected prod default to use `run preview` and --skip-frontend-build to use `run start`" + + +def test_skip_build_reuses_existing_build_and_requires_build_id() -> None: + serve = SERVE_SH.read_text(encoding="utf-8") + + assert 'if [ ! -f "$REPO_ROOT/frontend/.next/BUILD_ID" ]; then' in serve + assert "Run 'make start' once (full build)" in serve + + +def test_skip_build_preflight_runs_before_stop_all() -> None: + serve = SERVE_SH.read_text(encoding="utf-8") + + assert serve.index("frontend/.next/BUILD_ID") < serve.index('if [ "$ACTION" = "restart" ]; then') + + +def test_make_start_exposes_flag_as_opt_in() -> None: + for target in ("start", "start-daemon"): + recipe = _make_recipe(target) + assert "--skip-frontend-build" in recipe + assert "$(if $(filter 1,$(SKIP_FRONTEND_BUILD)),--skip-frontend-build)" in recipe diff --git a/scripts/serve.sh b/scripts/serve.sh index 1eb76aaa3..aa72d14b5 100755 --- a/scripts/serve.sh +++ b/scripts/serve.sh @@ -11,9 +11,11 @@ # --daemon Run all services in background (nohup), exit after startup # # Actions: -# --skip-install Skip dependency installation (faster restart) -# --stop Stop all running services and exit -# --restart Stop all services, then start with the given mode flags +# --skip-install Skip dependency installation (faster restart) +# --skip-frontend-build With --prod, reuse the existing .next build via `next start` +# instead of `next build` (opt-in; fails if no build exists) +# --stop Stop all running services and exit +# --restart Stop all services, then start with the given mode flags # # Examples: # ./scripts/serve.sh --dev # Gateway dev, hot reload @@ -53,6 +55,7 @@ _pick_python() { DEV_MODE=true DAEMON_MODE=false SKIP_INSTALL=false +SKIP_FRONTEND_BUILD=false ACTION="start" # start | stop | restart for arg in "$@"; do @@ -61,11 +64,12 @@ for arg in "$@"; do --prod) DEV_MODE=false ;; --daemon) DAEMON_MODE=true ;; --skip-install) SKIP_INSTALL=true ;; + --skip-frontend-build) SKIP_FRONTEND_BUILD=true ;; --stop) ACTION="stop" ;; --restart) ACTION="restart" ;; *) echo "Unknown argument: $arg" - echo "Usage: $0 [--dev|--prod] [--daemon] [--skip-install] [--stop|--restart]" + echo "Usage: $0 [--dev|--prod] [--daemon] [--skip-install] [--skip-frontend-build] [--stop|--restart]" exit 1 ;; esac @@ -268,6 +272,16 @@ stop_all() { echo "✓ All services stopped" } +# Validate the reusable frontend build before any stop_all runs, so start and +# restart never tear down a healthy stack only to fail here. --stop is exempt. +if [ "$ACTION" != "stop" ] && ! $DEV_MODE && $SKIP_FRONTEND_BUILD; then + if [ ! -f "$REPO_ROOT/frontend/.next/BUILD_ID" ]; then + echo "✗ --skip-frontend-build requires an existing frontend build." + echo " Run 'make start' once (full build), or: cd frontend && pnpm run build" + exit 1 + fi +fi + # ── Action routing ─────────────────────────────────────────────────────────── if [ "$ACTION" = "stop" ]; then @@ -305,6 +319,12 @@ export DEERFLOW_PNPM_PYTHON DEERFLOW_PNPM_RUNNER # Frontend command if $DEV_MODE; then FRONTEND_CMD='env PORT=3000 "$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev' + if $SKIP_FRONTEND_BUILD; then + echo " Note: --skip-frontend-build is ignored in dev mode (next dev does not build)." + fi +elif $SKIP_FRONTEND_BUILD; then + # The BUILD_ID preflight above already guarantees a reusable build exists. + FRONTEND_CMD="env PORT=3000 BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run start" else FRONTEND_CMD="env PORT=3000 BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run preview" fi @@ -405,6 +425,9 @@ echo " Starting DeerFlow" echo "==========================================" echo "" echo " Mode: $MODE_LABEL" +if ! $DEV_MODE && $SKIP_FRONTEND_BUILD; then + echo " (frontend: reusing existing build)" +fi echo "" echo " Services:" echo " Gateway → localhost:8001 (REST API + agent runtime)"