diff --git a/codex_context.yaml b/codex_context.yaml index f812f21..c4a0a09 100644 --- a/codex_context.yaml +++ b/codex_context.yaml @@ -75,6 +75,9 @@ SESSION_STATE: D15: "Extended ./deploy_agent.py with deploy_service(service): generates compose, writes ./deployments//docker-compose.yml without overwriting existing directories, runs 'docker compose up -d' via subprocess, returns DEPLOYED or ERROR." D16: "Updated ./deploy_agent.py with get_service_status(path), post-deploy 'docker compose ps' verification requiring 'Up', error outputs including ps output when available, and pre-deploy 'docker ps' port-80 check that adds prompt note 'Use a different port than 80'." D17: "User requested git commit on 2026-04-22; commit scope includes ./deploy_agent.py and ./codex_context.yaml for deployment status and safety updates." + D18: "Git commit created on 2026-04-22: 0abe9cb 'Improve deploy agent safety checks'." + D19: "Updated ./deploy_agent.py to use local LLM for one bounded deployment-failure retry: capture service/error/status, request corrected YAML only, replace docker-compose.yml, retry once, then return final error plus last status if still failing." + D20: "User requested git commit on 2026-04-22; commit scope includes ./deploy_agent.py and ./codex_context.yaml for one-shot LLM-assisted deployment failure recovery." todos: T1: "For all future meaningful changes/decisions, update and overwrite ./codex_context.yaml." T2: "DONE: Commit current changes." @@ -89,6 +92,8 @@ SESSION_STATE: T11: "DONE: Add docker-compose YAML validation and retry logic." T12: "DONE: Add automatic service deployment workflow." T13: "DONE: Add deployment status verification and basic port-80 safety check." + T14: "DONE: Commit deploy agent safety/status updates." + T15: "DONE: Add one-shot LLM-assisted deployment failure recovery." issues: I1: "Tailscale DNS health warning: configured DNS servers unreachable." I2: "Preferred gateway path unavailable: 100.108.208.3:8080 connection failed." diff --git a/deploy_agent.py b/deploy_agent.py index 96a96fc..02f7001 100644 --- a/deploy_agent.py +++ b/deploy_agent.py @@ -43,6 +43,30 @@ def generate_compose(service: str) -> str: return _generate_compose(service) +def _is_valid_compose(compose: str) -> bool: + try: + parsed = yaml.safe_load(compose) + except yaml.YAMLError: + return False + return isinstance(parsed, dict) and "services" in parsed + + +def _fix_compose(service: str, error: str, status: str) -> str: + prompt = ( + "Deployment failed.\n\n" + f"Service: {service}\n\n" + f"Error: {error}\n\n" + f"Status: {status}\n\n" + "Fix the docker-compose YAML. Return ONLY corrected YAML." + ) + response = ask(prompt) + if response.startswith("ERROR:"): + return response + if not _is_valid_compose(response): + return "ERROR: invalid docker-compose" + return response + + def get_service_status(path: Path) -> str: try: result = subprocess.run( @@ -61,6 +85,23 @@ def get_service_status(path: Path) -> str: return output +def _run_compose_up(path: Path) -> tuple[bool, str]: + try: + result = subprocess.run( + ["docker", "compose", "up", "-d"], + cwd=path, + check=False, + capture_output=True, + text=True, + ) + except Exception as exc: + return False, str(exc) + if result.returncode != 0: + error = result.stderr.strip() or result.stdout.strip() or "docker compose up failed" + return False, error + return True, result.stdout.strip() + + def deploy_service(service: str) -> str: prompt_extra = "" try: @@ -91,19 +132,21 @@ def deploy_service(service: str) -> str: compose_path = target_dir / "docker-compose.yml" compose_path.write_text(compose, encoding="utf-8") - try: - subprocess.run( - ["docker", "compose", "up", "-d"], - cwd=target_dir, - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as exc: - message = exc.stderr.strip() or exc.stdout.strip() or str(exc) - return f"ERROR: {message}" - except Exception as exc: - return f"ERROR: {exc}" + ok, error = _run_compose_up(target_dir) + if not ok: + status = get_service_status(target_dir) + fixed_compose = _fix_compose(service, error, status) + if fixed_compose.startswith("ERROR:"): + if status and not status.startswith("ERROR:"): + return f"ERROR: {error}\n{status}" + return f"ERROR: {error}" + compose_path.write_text(fixed_compose, encoding="utf-8") + ok, error = _run_compose_up(target_dir) + if not ok: + status = get_service_status(target_dir) + if status and not status.startswith("ERROR:"): + return f"ERROR: {error}\n{status}" + return f"ERROR: {error}" status = get_service_status(target_dir) if status.startswith("ERROR:"):