fix(commit): stop running macos-container bottle before committing
lint / lint (push) Successful in 1m38s
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 17s

`container export` requires the container to be stopped first. When a
running bottle is detected, prompt the user to confirm, stop the
container, then commit. Adds `container_is_running` and
`stop_container` helpers to the macos-container util.

Addresses #240 (comment)
This commit is contained in:
2026-06-23 07:22:33 +00:00
parent 81ce23a54d
commit 3cd4a7acd9
3 changed files with 98 additions and 1 deletions
@@ -252,6 +252,36 @@ def container_exists(name: str) -> bool:
return name in {line.strip() for line in result.stdout.splitlines()} return name in {line.strip() for line in result.stdout.splitlines()}
def container_is_running(name: str) -> bool:
"""Return True if the named container is currently running.
`container list` without `--all` lists only running containers."""
result = subprocess.run(
[_CONTAINER, "list", "--quiet"],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
return False
return name in {line.strip() for line in result.stdout.splitlines()}
def stop_container(name: str) -> None:
"""Stop the named container without deleting it."""
result = subprocess.run(
[_CONTAINER, "stop", name],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
die(
f"container stop {name!r} failed: "
f"{(result.stderr or '').strip() or '<no stderr>'}"
)
def force_remove_container(name: str) -> None: def force_remove_container(name: str) -> None:
if container_exists(name): if container_exists(name):
subprocess.run( subprocess.run(
+15 -1
View File
@@ -11,16 +11,19 @@ snapshot instead of rebuilding from the Dockerfile.
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import sys
from pathlib import Path from pathlib import Path
from ..backend import enumerate_active_agents from ..backend import enumerate_active_agents
from ..backend.docker.util import commit_container as docker_commit_container from ..backend.docker.util import commit_container as docker_commit_container
from ..backend.macos_container.util import commit_container as macos_commit_container from ..backend.macos_container.util import commit_container as macos_commit_container
from ..backend.macos_container.util import container_is_running as macos_container_is_running
from ..backend.macos_container.util import stop_container as macos_stop_container
from ..backend.smolmachines.smolvm import pack_create_from_vm from ..backend.smolmachines.smolvm import pack_create_from_vm
from ..bottle_state import bottle_state_dir from ..bottle_state import bottle_state_dir
from ..bottle_state import mark_preserved, read_metadata, write_committed_image from ..bottle_state import mark_preserved, read_metadata, write_committed_image
from ..log import die, info from ..log import die, info
from ._common import PROG from ._common import PROG, read_tty_line
from . import tui from . import tui
@@ -91,6 +94,17 @@ def cmd_commit(argv: list[str]) -> int:
container = _agent_container_name(slug) container = _agent_container_name(slug)
image_tag = _committed_image_tag(slug) image_tag = _committed_image_tag(slug)
if macos_container_is_running(container):
sys.stderr.write(
f"bot-bottle: bottle {slug!r} is running; "
"commit will stop it. Continue? [y/N] "
)
sys.stderr.flush()
reply = read_tty_line().strip().lower()
if reply not in ("y", "yes"):
return 0
macos_stop_container(container)
macos_commit_container(container, image_tag) macos_commit_container(container, image_tag)
write_committed_image(slug, image_tag) write_committed_image(slug, image_tag)
mark_preserved(slug) mark_preserved(slug)
+53
View File
@@ -143,6 +143,8 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
"bot_bottle.cli.commit.macos_commit_container", "bot_bottle.cli.commit.macos_commit_container",
) as mock_commit, patch( ) as mock_commit, patch(
"bot_bottle.cli.commit.info", "bot_bottle.cli.commit.info",
), patch(
"bot_bottle.cli.commit.macos_container_is_running", return_value=False,
): ):
rc = cmd_commit([slug]) rc = cmd_commit([slug])
@@ -152,6 +154,57 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
f"bot-bottle-committed-{slug}:latest", f"bot-bottle-committed-{slug}:latest",
) )
def test_running_macos_container_stops_then_commits_on_yes(self):
slug = "dev-abc12"
bottle_state.write_metadata(bottle_state.BottleMetadata(
identity=slug, agent_name="dev", cwd="", copy_cwd=False,
started_at="t", backend="macos-container",
))
with patch(
"bot_bottle.cli.commit.macos_container_is_running", return_value=True,
), patch(
"bot_bottle.cli.commit.read_tty_line", return_value="y",
), patch(
"bot_bottle.cli.commit.macos_stop_container",
) as mock_stop, patch(
"bot_bottle.cli.commit.macos_commit_container",
) as mock_commit, patch(
"bot_bottle.cli.commit.info",
):
rc = cmd_commit([slug])
self.assertEqual(0, rc)
mock_stop.assert_called_once_with(f"bot-bottle-{slug}")
mock_commit.assert_called_once_with(
f"bot-bottle-{slug}",
f"bot-bottle-committed-{slug}:latest",
)
def test_running_macos_container_aborts_on_no(self):
slug = "dev-abc12"
bottle_state.write_metadata(bottle_state.BottleMetadata(
identity=slug, agent_name="dev", cwd="", copy_cwd=False,
started_at="t", backend="macos-container",
))
with patch(
"bot_bottle.cli.commit.macos_container_is_running", return_value=True,
), patch(
"bot_bottle.cli.commit.read_tty_line", return_value="n",
), patch(
"bot_bottle.cli.commit.macos_stop_container",
) as mock_stop, patch(
"bot_bottle.cli.commit.macos_commit_container",
) as mock_commit, patch(
"bot_bottle.cli.commit.info",
):
rc = cmd_commit([slug])
self.assertEqual(0, rc)
mock_stop.assert_not_called()
mock_commit.assert_not_called()
class TestCmdCommitSmolmachinesBackend(_FakeHomeMixin, unittest.TestCase): class TestCmdCommitSmolmachinesBackend(_FakeHomeMixin, unittest.TestCase):
def setUp(self): def setUp(self):