diff --git a/Makefile b/Makefile index df76b54c6..5d7d0d74a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # DeerFlow - Unified Development Environment -.PHONY: help config config-upgrade check check-agent-guidance install extension-install extension-upgrade extension-list extension-enable extension-disable extension-remove setup doctor support-bundle detect-thread-boundaries detect-blocking-io dev dev-daemon start start-daemon nginx stop up down clean docker-init docker-start docker-stop docker-logs docker-logs-frontend docker-logs-gateway docker-logs-redis setup-sandbox +.PHONY: help config config-upgrade check check-agent-guidance install extension-install extension-upgrade extension-list extension-enable extension-disable extension-remove setup doctor support-bundle detect-thread-boundaries detect-blocking-io dev dev-daemon start start-daemon nginx stop up down prod-logs clean docker-init docker-start docker-stop docker-logs docker-logs-frontend docker-logs-gateway docker-logs-redis setup-sandbox BASH ?= bash BACKEND_UV_RUN = cd backend && uv run @@ -50,6 +50,7 @@ help: @echo "" @echo "Docker Production Commands:" @echo " make up - Build and start production Docker services (localhost:2026)" + @echo " make prod-logs - Follow production Docker logs (stack started by 'make up')" @echo " make down - Stop and remove production Docker containers" @echo "" @echo "Docker Development Commands:" @@ -217,3 +218,7 @@ up: # Stop and remove production containers down: @$(RUN_SHELL_SCRIPT) ./scripts/deploy.sh down + +# Follow production container logs (stack started by `make up`) +prod-logs: + @$(RUN_SHELL_SCRIPT) ./scripts/docker.sh logs --prod diff --git a/backend/tests/test_docker_sandbox_mode_detection.py b/backend/tests/test_docker_sandbox_mode_detection.py index dc70f3b86..9c1bccb28 100644 --- a/backend/tests/test_docker_sandbox_mode_detection.py +++ b/backend/tests/test_docker_sandbox_mode_detection.py @@ -313,6 +313,83 @@ require_compose_version assert "too old" in result.stdout +@pytest.mark.parametrize("args", ["logs --prod", "logs --prod --gateway"]) +def test_logs_prod_targets_production_stack(args): + """`logs --prod` must tail the stack deploy.sh started, not the dev project. + + `make up` runs scripts/deploy.sh (project `deer-flow`, docker-compose.yaml) + while the dev default is project `deer-flow-dev`, so `make docker-logs` + after `make up` printed nothing (#5529). The production entry point must + target the same project and interpolate the same .env. + + Compose detection is NOT stubbed here: it rebuilds COMPOSE_CMD, and the + appended `--env-file` must survive that rebuild (#5538 review). + """ + with tempfile.TemporaryDirectory() as tmpdir: + tmp_root = Path(tmpdir) + (tmp_root / "docker-compose.yaml").write_text("services: {}\n", encoding="utf-8") + (tmp_root / ".env").write_text("KEEP=me\n", encoding="utf-8") + marker = tmp_root / "prod_invoke.txt" + + command = f""" +source '{SCRIPT_PATH}' +PROJECT_ROOT='{tmp_root}' +DOCKER_DIR='{tmp_root}' +docker() {{ + if [ "$1" = compose ] && [ "$2" = version ]; then + echo '2.41.0' + return 0 + fi + if [ "$1" = compose ]; then + printf '%s\\n' "$*" "DEER_FLOW_HOME=${{DEER_FLOW_HOME:-unset}}" > '{marker}' + return 0 + fi + command docker "$@" +}} +unset DEER_FLOW_ROOT DEER_FLOW_HOME DEER_FLOW_CONFIG_PATH +unset DEER_FLOW_EXTENSIONS_CONFIG_PATH DEER_FLOW_REPO_ROOT +unset BETTER_AUTH_SECRET DEER_FLOW_INTERNAL_AUTH_TOKEN +{args} +""" + subprocess.check_call([BASH_EXECUTABLE, "-lc", command]) + + recorded = marker.read_text(encoding="utf-8") + assert "-p deer-flow " in recorded, recorded + assert "-f docker-compose.yaml" in recorded, recorded + assert "--env-file ../.env" in recorded, recorded + assert "logs" in recorded, recorded + # deploy.sh exports interpolation defaults before every compose call; + # `logs --prod` must too, or the volume specs fail to parse. + assert "DEER_FLOW_HOME=" in recorded, recorded + assert "DEER_FLOW_HOME=unset" not in recorded, recorded + + +def test_logs_without_dev_containers_hints_at_prod_logs(): + """Silent empty output is the #5529 report: point at the production entry point.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmp_root = Path(tmpdir) + _seed_compose_file(tmp_root) + + command = f""" +source '{SCRIPT_PATH}' +PROJECT_ROOT='{tmp_root}' +DOCKER_DIR='{tmp_root}' +require_compose_version() {{ :; }} +COMPOSE_CMD=true +unset DEER_FLOW_ROOT +logs +""" + result = subprocess.run( + [BASH_EXECUTABLE, "-lc", command], + capture_output=True, + text=True, + encoding="utf-8", + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert "make prod-logs" in result.stdout + + def test_aio_dood_socket_preflight_allows_windows_when_docker_reachable(): """Windows Git Bash without /var/run/docker.sock proceeds when Docker daemon is reachable.""" with tempfile.TemporaryDirectory() as tmpdir: diff --git a/scripts/docker.sh b/scripts/docker.sh index 29114267b..c0352e1ce 100755 --- a/scripts/docker.sh +++ b/scripts/docker.sh @@ -16,12 +16,15 @@ DOCKER_DIR="$PROJECT_ROOT/docker" # Use a filename relative to DOCKER_DIR (we always `cd` there) so Windows # Docker Desktop does not receive a Git Bash `/c/...` path it cannot open. COMPOSE_FILE="docker-compose-dev.yaml" +# Dev stack project name. `logs --prod` swaps both values for the production +# stack started by `make up` (scripts/deploy.sh: project `deer-flow`). +COMPOSE_PROJECT="deer-flow-dev" # Selected by require_compose_version: prefer the V2 plugin, else hyphenated binary. # Kept as an array so "docker compose" stays two words under set -u / quoting. COMPOSE_BIN=(docker compose) _refresh_compose_cmd() { - COMPOSE_CMD="${COMPOSE_BIN[*]} -p deer-flow-dev -f ${COMPOSE_FILE}" + COMPOSE_CMD="${COMPOSE_BIN[*]} -p ${COMPOSE_PROJECT} -f ${COMPOSE_FILE}" } _refresh_compose_cmd @@ -416,43 +419,73 @@ start() { echo "" } -# View Docker development logs +# View Docker logs. The dev stack (make docker-start) is the default; +# `--prod` tails the production stack started by `make up` (scripts/deploy.sh). logs() { local service="" + local is_prod=0 + + while [ $# -gt 0 ]; do + case "$1" in + --prod) + is_prod=1 + ;; + --frontend|--gateway|--nginx|--redis|--provisioner) + if [ -n "$service" ]; then + echo -e "${YELLOW}Only one service option is allowed (got --$service and $1).${NC}" + exit 1 + fi + service="${1#--}" + ;; + *) + echo -e "${YELLOW}Unknown option: $1${NC}" + echo "Usage: $0 logs [--prod] [--frontend|--gateway|--nginx|--redis|--provisioner]" + exit 1 + ;; + esac + shift + done + + if [ "$is_prod" = 1 ]; then + # Target the same project deploy.sh started. Relative paths: this + # runs with cwd=$DOCKER_DIR. + COMPOSE_FILE="docker-compose.yaml" + COMPOSE_PROJECT="deer-flow" + # deploy.sh exports these before every compose invocation so the + # volume specs in docker-compose.yaml interpolate; without them even + # `logs` fails to parse the file on checkouts without a .env. + export DEER_FLOW_HOME="${DEER_FLOW_HOME:-$PROJECT_ROOT/backend/.deer-flow}" + export DEER_FLOW_CONFIG_PATH="${DEER_FLOW_CONFIG_PATH:-$DEER_FLOW_HOME/config.yaml}" + export DEER_FLOW_EXTENSIONS_CONFIG_PATH="${DEER_FLOW_EXTENSIONS_CONFIG_PATH:-$DEER_FLOW_HOME/extensions_config.json}" + export DEER_FLOW_REPO_ROOT="${DEER_FLOW_REPO_ROOT:-$PROJECT_ROOT}" + export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-placeholder}" + export DEER_FLOW_INTERNAL_AUTH_TOKEN="${DEER_FLOW_INTERNAL_AUTH_TOKEN:-placeholder}" + elif [ -z "$service" ]; then + # The dev and production stacks use different compose projects, so + # `make docker-logs` after `make up` would exit with empty output; + # point at the production entry point instead of staying silent. + if [ -z "$(cd "$DOCKER_DIR" && $COMPOSE_CMD ps -q 2>/dev/null)" ]; then + echo -e "${YELLOW}No dev containers are running.${NC}" + echo "Started the production stack with 'make up'? View its logs with 'make prod-logs'." + fi + fi compose_preflight - case "$1" in - --frontend) - service="frontend" - echo -e "${BLUE}Viewing frontend logs...${NC}" - ;; - --gateway) - service="gateway" - echo -e "${BLUE}Viewing gateway logs...${NC}" - ;; - --nginx) - service="nginx" - echo -e "${BLUE}Viewing nginx logs...${NC}" - ;; - --redis) - service="redis" - echo -e "${BLUE}Viewing redis logs...${NC}" - ;; - --provisioner) - service="provisioner" - echo -e "${BLUE}Viewing provisioner logs...${NC}" - ;; - "") - echo -e "${BLUE}Viewing all logs...${NC}" - ;; - *) - echo -e "${YELLOW}Unknown option: $1${NC}" - echo "Usage: $0 logs [--frontend|--gateway|--nginx|--redis|--provisioner]" - exit 1 - ;; - esac - + # Append --env-file only after compose_preflight(): its Compose detection + # may rebuild COMPOSE_CMD, which would drop anything appended before it. + if [ "$is_prod" = 1 ] && [ -f "$PROJECT_ROOT/.env" ]; then + COMPOSE_CMD="$COMPOSE_CMD --env-file ../.env" + fi + + if [ -n "$service" ]; then + echo -e "${BLUE}Viewing $service logs...${NC}" + elif [ "$is_prod" = 1 ]; then + echo -e "${BLUE}Viewing production stack logs...${NC}" + else + echo -e "${BLUE}Viewing all logs...${NC}" + fi + cd "$DOCKER_DIR" && $COMPOSE_CMD logs -f $service } @@ -493,7 +526,8 @@ help() { echo " init - Pull the sandbox image (speeds up first Pod startup)" echo " start - Start Docker services (auto-detects sandbox mode from config.yaml)" echo " restart - Restart all running Docker services" - echo " logs [option] - View Docker development logs" + echo " logs [option] - View Docker logs (dev stack by default, production with --prod)" + echo " --prod View production stack logs (containers from 'make up')" echo " --frontend View frontend logs only" echo " --gateway View gateway logs only" echo " --nginx View nginx logs only" @@ -518,7 +552,8 @@ main() { restart ;; logs) - logs "$2" + shift + logs "$@" ;; stop) stop