fix(scripts): select sandbox containers by anchored name (#7189)

This commit is contained in:
Tsai Yuan 2026-10-21 20:57:02 +08:00 committed by GitHub
parent 702528e5c3
commit c70dd685a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 9 deletions

View File

@ -770,6 +770,10 @@ DeerFlow runs the agent runtime inside the Gateway API. Development mode enables
| **Stop** | `./scripts/serve.sh --stop`<br/>`make stop` | `./scripts/docker.sh stop`<br/>`make docker-stop` | `./scripts/deploy.sh down`<br/>`make down` |
| **Restart** | `./scripts/serve.sh --restart [flags]` | `./scripts/docker.sh restart` | — |
Local and Docker development stop commands also stop running sandbox containers
whose names start with `deer-flow-sandbox-`. Containers with that text elsewhere
in their names are left alone.
`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

View File

@ -0,0 +1,62 @@
"""Offline checks for the names selected by the sandbox cleanup helper."""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
import pytest
SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "cleanup-containers.sh"
def _stub(path: Path, name: str, source: str) -> None:
executable = path / name
executable.write_text("#!/bin/sh\n" + source, encoding="utf-8", newline="\n")
executable.chmod(0o755)
@pytest.mark.skipif(os.name == "nt", reason="POSIX executable stubs")
def test_docker_cleanup_stops_only_sandbox_prefix(tmp_path: Path) -> None:
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
stop_log = tmp_path / "stopped"
_stub(
bin_dir,
"docker",
'if [ "$1" = ps ]; then\n'
' printf "other-id not-deer-flow-sandbox Up 3 days\\nvalid-id deer-flow-sandbox-abc Up 1 hour\\nbackup-id deer-flow-sandboxbackup Up 2 days\\n"\n'
'elif [ "$1" = stop ]; then\n'
' shift; printf "%s\\n" "$@" >> "$STOP_LOG"\n'
"fi\n",
)
env = {**os.environ, "PATH": f"{bin_dir}:/usr/bin:/bin", "STOP_LOG": str(stop_log)}
result = subprocess.run(["bash", str(SCRIPT), "deer-flow-sandbox"], env=env, capture_output=True, text=True, timeout=5)
assert result.returncode == 0, result.stderr
assert stop_log.read_text(encoding="utf-8").splitlines() == ["valid-id"]
assert "not-deer-flow-sandbox" not in result.stdout
assert "deer-flow-sandbox-abc Up 1 hour" in result.stdout
@pytest.mark.skipif(os.name == "nt", reason="POSIX executable stubs")
def test_apple_cleanup_ignores_unrelated_names_and_bad_entries(tmp_path: Path) -> None:
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
stop_log = tmp_path / "stopped"
entries = [
{"configuration": None},
{"configuration": {"id": "not-deer-flow-sandbox"}},
{"configuration": {"id": "deer-flow-sandbox-abc"}},
]
payload = tmp_path / "containers.json"
payload.write_text(json.dumps(entries), encoding="utf-8")
_stub(bin_dir, "container", 'if [ "$1" = list ]; then cat "$CONTAINERS_JSON"; elif [ "$1" = stop ]; then printf "%s\\n" "$2" >> "$STOP_LOG"; fi\n')
env = {**os.environ, "PATH": f"{bin_dir}:/usr/bin:/bin", "STOP_LOG": str(stop_log), "CONTAINERS_JSON": str(payload)}
result = subprocess.run(["bash", str(SCRIPT), "deer-flow-sandbox"], env=env, capture_output=True, text=True, timeout=5)
assert result.returncode == 0, result.stderr
assert stop_log.read_text(encoding="utf-8").splitlines() == ["deer-flow-sandbox-abc"]
assert "not-deer-flow-sandbox" not in result.stdout

View File

@ -214,6 +214,11 @@ runs `check-data-not-in-use.sh` before `make stop` (which would stop a live
stack's sandboxes) and refuses while that container runs; an absent or
unreachable Docker passes. `make stop` must still run before the delete: it
stops `deer-flow-sandbox*` containers, whose thread mounts live in that tree.
`cleanup-containers.sh` must verify an anchored `<prefix>-` name after Docker's
substring `name=` filter, and use the same rule for Apple Container IDs. Parse
Apple Container JSON without interpolating the prefix into Python source; bad
entries must not hide later valid containers. Offline stub coverage lives in
`backend/tests/test_cleanup_containers_script.py`.
`RUNTIME_DATA_CONTENTS` feeds both the help line and the deletion notice.
Host-side pnpm calls must go through `scripts/pnpm.py`. With native Windows

View File

@ -22,14 +22,23 @@ echo "Cleaning up sandbox containers with prefix: ${PREFIX}"
cleanup_docker() {
if command -v docker &> /dev/null; then
echo -n "Checking Docker containers... "
DOCKER_CONTAINERS=$(docker ps -q --filter "name=${PREFIX}" 2>/dev/null || echo "")
# Docker's name filter matches substrings. Check the returned names as
# well so a container like not-deer-flow-sandbox is never stopped.
DOCKER_LIST=$(docker ps --filter "name=${PREFIX}-" --format '{{.ID}} {{.Names}} {{.Status}}' 2>/dev/null || true)
docker_ids=()
docker_rows=()
while read -r cid name status; do
case "$name" in
"$PREFIX"-*) docker_ids+=("$cid"); docker_rows+=("$cid $name $status") ;;
esac
done <<< "$DOCKER_LIST"
if [ -n "$DOCKER_CONTAINERS" ]; then
if [ "${#docker_ids[@]}" -gt 0 ]; then
echo ""
echo "Found Docker containers to clean up:"
docker ps --filter "name=${PREFIX}" --format "table {{.ID}}\t{{.Names}}\t{{.Status}}"
printf ' %s\n' "${docker_rows[@]}"
echo "Stopping Docker containers..."
echo "$DOCKER_CONTAINERS" | xargs docker stop 2>/dev/null || true
docker stop "${docker_ids[@]}" >/dev/null 2>&1 || true
echo -e "${GREEN}✓ Docker containers stopped${NC}"
else
echo -e "${GREEN}none found${NC}"
@ -49,21 +58,23 @@ cleanup_apple_container() {
if [ "$CONTAINER_LIST" != "[]" ] && [ -n "$CONTAINER_LIST" ]; then
# Extract container IDs that match our prefix
CONTAINER_IDS=$(echo "$CONTAINER_LIST" | python3 -c "
CONTAINER_IDS=$(echo "$CONTAINER_LIST" | CLEANUP_CONTAINER_PREFIX="$PREFIX" python3 -c '
import json
import os
import sys
try:
containers = json.load(sys.stdin)
if isinstance(containers, list):
prefix = os.environ["CLEANUP_CONTAINER_PREFIX"] + "-"
for c in containers:
if isinstance(c, dict):
# Apple Container uses 'id' field which contains the container name
cid = c.get('configuration').get('id', '')
if '${PREFIX}' in cid:
configuration = c.get("configuration")
cid = configuration.get("id") if isinstance(configuration, dict) else None
if isinstance(cid, str) and cid.startswith(prefix):
print(cid)
except:
pass
" 2>/dev/null || echo "")
' 2>/dev/null || echo "")
if [ -n "$CONTAINER_IDS" ]; then
echo ""