From f80c96be55296d0f6184a9fdff8fbe0409a23a46 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sun, 22 Feb 2026 17:06:36 +0100
Subject: [PATCH] fix: rebuild/recreate — use ops CLI instead of Coolify API

---
 app/routers/rebuild.py |  176 +++++++++++++++++++++-------------------------------------
 1 files changed, 63 insertions(+), 113 deletions(-)

diff --git a/app/routers/rebuild.py b/app/routers/rebuild.py
index d4873a4..447979f 100644
--- a/app/routers/rebuild.py
+++ b/app/routers/rebuild.py
@@ -19,6 +19,7 @@
 
 from app.auth import verify_token
 from app.ops_runner import (
+    OPS_CLI,
     _BACKUP_TIMEOUT,
     run_command,
     run_command_host,
@@ -298,68 +299,41 @@
 
 async def _op_rebuild(project: str, env: str) -> AsyncGenerator[str, None]:
     """
-    Rebuild: Coolify stop → docker build → Coolify start.
+    Rebuild: docker compose down → build image → docker compose up.
+    Uses `ops rebuild` on the host which handles env files, profiles, and cd correctly.
     No data loss. For code/Dockerfile changes.
     """
-    try:
-        uuid = _coolify_uuid(project, env)
-        build = _build_cfg(project, env)
-    except ValueError as exc:
-        yield _line(f"[error] Config error: {exc}")
-        yield _done(False, project, env, "rebuild")
-        return
+    yield _line(f"[rebuild] Rebuilding {project}/{env} via ops CLI...")
 
-    # Step 1: Stop via Coolify
-    yield _line(f"[rebuild] Stopping {project}/{env} via Coolify (uuid={uuid})...")
-    try:
-        await _coolify_action("stop", uuid)
-        yield _line(f"[rebuild] Coolify stop queued. Waiting for containers to stop...")
-        # Step 2: Poll until stopped
-        stopped = await _poll_until_stopped(project, env)
-        if not stopped:
-            yield _line(f"[warn] Containers may still be running after {_POLL_MAX_WAIT}s — proceeding anyway")
-        else:
-            yield _line(f"[rebuild] All containers stopped.")
-    except RuntimeError as exc:
-        if "already stopped" in str(exc).lower():
-            yield _line(f"[rebuild] Service already stopped — continuing with build.")
-        else:
-            yield _line(f"[error] Coolify stop failed: {exc}")
-            yield _done(False, project, env, "rebuild")
-            return
-
-    # Step 3: Build image (if project uses local images)
-    if build:
-        ctx = build["build_context"]
-        image = f"{build['image_name']}:{env}"
-        yield _line(f"[rebuild] Building Docker image: {image}")
-        yield _line(f"[rebuild] Build context: {ctx}")
-
-        async for line in stream_command_host(
-            ["docker", "build", "-t", image, ctx],
-            timeout=_BACKUP_TIMEOUT,
-        ):
+    had_output = False
+    success = True
+    async for line in stream_command_host(
+        [OPS_CLI, "rebuild", project, env],
+        timeout=_BACKUP_TIMEOUT,
+    ):
+        had_output = True
+        if line.startswith("[stderr] "):
             yield _line(line)
-    else:
-        yield _line(f"[rebuild] No local image build needed (registry images only).")
+        elif line.startswith("ERROR") or line.startswith("[error]"):
+            yield _line(f"[error] {line}")
+            success = False
+        else:
+            yield _line(f"[rebuild] {line}")
 
-    # Step 4: Start via Coolify
-    yield _line(f"[rebuild] Starting {project}/{env} via Coolify...")
-    try:
-        await _coolify_action("start", uuid)
-        yield _line(f"[rebuild] Coolify start queued. Waiting for containers...")
-    except RuntimeError as exc:
-        yield _line(f"[error] Coolify start failed: {exc}")
-        yield _done(False, project, env, "rebuild")
-        return
+    if not had_output:
+        yield _line(f"[error] ops rebuild produced no output — check registry config for {project}")
+        success = False
 
-    # Step 5: Poll until running
-    running = await _poll_until_running(project, env)
-    if running:
-        yield _line(f"[rebuild] Containers are up.")
-        yield _done(True, project, env, "rebuild")
+    if success:
+        # Verify containers came up
+        containers = await _find_containers_for_service(project, env)
+        if containers:
+            yield _line(f"[rebuild] {len(containers)} container(s) running: {', '.join(containers)}")
+            yield _done(True, project, env, "rebuild")
+        else:
+            yield _line(f"[warn] No containers found after rebuild — check docker compose logs")
+            yield _done(False, project, env, "rebuild")
     else:
-        yield _line(f"[warn] Containers did not appear healthy within {_POLL_MAX_WAIT}s — check Coolify logs.")
         yield _done(False, project, env, "rebuild")
 
 
@@ -369,12 +343,10 @@
 
 async def _op_recreate(project: str, env: str) -> AsyncGenerator[str, None]:
     """
-    Recreate: Coolify stop → wipe data → docker build → Coolify start.
+    Recreate: docker compose down → wipe data → docker build → docker compose up.
     DESTRUCTIVE — wipes all data volumes. Shows "Go to Backups" banner on success.
     """
     try:
-        uuid = _coolify_uuid(project, env)
-        build = _build_cfg(project, env)
         data_dir = _data_dir(project, env)
         cfg = _project_cfg(project)
     except ValueError as exc:
@@ -382,42 +354,35 @@
         yield _done(False, project, env, "recreate")
         return
 
-    # Step 1: Stop via Coolify
-    yield _line(f"[recreate] Stopping {project}/{env} via Coolify (uuid={uuid})...")
-    try:
-        await _coolify_action("stop", uuid)
-        yield _line(f"[recreate] Coolify stop queued. Waiting for containers to stop...")
-        # Step 2: Poll until stopped
-        stopped = await _poll_until_stopped(project, env)
-        if not stopped:
-            yield _line(f"[warn] Containers may still be running after {_POLL_MAX_WAIT}s — proceeding anyway")
-        else:
-            yield _line(f"[recreate] All containers stopped.")
-    except RuntimeError as exc:
-        if "already stopped" in str(exc).lower():
-            yield _line(f"[recreate] Service already stopped — skipping stop step.")
-        else:
-            yield _line(f"[error] Coolify stop failed: {exc}")
-            yield _done(False, project, env, "recreate")
-            return
+    # Step 1: Find and stop containers via docker compose
+    code_dir = cfg.get("path", "") + f"/{env}/code"
+    yield _line(f"[recreate] Stopping {project}/{env} containers...")
 
-    # Step 3: Wipe data volumes
-    yield _line(f"[recreate] WARNING: Wiping data directory: {data_dir}")
-    # Verify THIS env's containers are actually stopped before wiping
+    stop_result = await run_command_host(
+        ["sh", "-c", f"cd {code_dir} && docker compose -p {env}-{cfg.get('name_prefix', project)} --profile {env} down 2>&1 || true"],
+        timeout=120,
+    )
+    if stop_result["output"].strip():
+        for line in stop_result["output"].strip().splitlines():
+            yield _line(line)
+
+    # Step 2: Verify containers are stopped
     name_prefix = cfg.get("name_prefix", project)
-    # Use grep to AND-match both prefix and env (docker --filter uses OR for multiple name filters)
     verify = await run_command_host(
-        ["sh", "-c", f"docker ps --format '{{{{.Names}}}}' | grep '^{env}-{name_prefix}\\|^{name_prefix}-{env}' || true"],
+        ["sh", "-c", f"docker ps --format '{{{{.Names}}}}' | grep '^{env}-{name_prefix}-' || true"],
         timeout=30,
     )
-    running = verify["output"].strip()
-    if running:
+    running_containers = verify["output"].strip()
+    if running_containers:
         yield _line(f"[error] Containers still running for {project}/{env}:")
-        for line in running.splitlines():
+        for line in running_containers.splitlines():
             yield _line(f"  {line}")
         yield _done(False, project, env, "recreate")
         return
+    yield _line(f"[recreate] All containers stopped.")
 
+    # Step 3: Wipe data volumes
+    yield _line(f"[recreate] WARNING: Wiping data directory: {data_dir}")
     wipe_result = await run_command_host(
         ["sh", "-c", f"rm -r {data_dir}/* 2>&1; echo EXIT_CODE=$?"],
         timeout=120,
@@ -432,39 +397,24 @@
         yield _done(False, project, env, "recreate")
         return
 
-    # Step 4: Build image (if project uses local images)
-    if build:
-        ctx = build["build_context"]
-        image = f"{build['image_name']}:{env}"
-        yield _line(f"[recreate] Building Docker image: {image}")
-        yield _line(f"[recreate] Build context: {ctx}")
-
-        async for line in stream_command_host(
-            ["docker", "build", "-t", image, ctx],
-            timeout=_BACKUP_TIMEOUT,
-        ):
+    # Step 4: Rebuild via ops CLI (handles image build + compose up)
+    yield _line(f"[recreate] Rebuilding containers...")
+    async for line in stream_command_host(
+        [OPS_CLI, "rebuild", project, env],
+        timeout=_BACKUP_TIMEOUT,
+    ):
+        if line.startswith("[stderr] "):
             yield _line(line)
-    else:
-        yield _line(f"[recreate] No local image build needed (registry images only).")
+        else:
+            yield _line(f"[recreate] {line}")
 
-    # Step 5: Start via Coolify
-    yield _line(f"[recreate] Starting {project}/{env} via Coolify...")
-    try:
-        await _coolify_action("start", uuid)
-        yield _line(f"[recreate] Coolify start queued. Waiting for containers...")
-    except RuntimeError as exc:
-        yield _line(f"[error] Coolify start failed: {exc}")
-        yield _done(False, project, env, "recreate")
-        return
-
-    # Step 6: Poll until running
-    running = await _poll_until_running(project, env)
-    if running:
-        yield _line(f"[recreate] Containers are up. Restore a backup to complete recovery.")
+    # Step 5: Verify containers came up
+    containers = await _find_containers_for_service(project, env)
+    if containers:
+        yield _line(f"[recreate] {len(containers)} container(s) running. Restore a backup to complete recovery.")
         yield _done(True, project, env, "recreate")
     else:
-        yield _line(f"[warn] Containers did not appear within {_POLL_MAX_WAIT}s — check Coolify logs.")
-        # Still return success=True so the "Go to Backups" banner appears
+        yield _line(f"[warn] No containers found after recreate — check docker compose logs")
         yield _done(True, project, env, "recreate")
 
 

--
Gitblit v1.3.1