Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/prime/src/prime_cli/commands/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ def push_image(
click_type=click.Choice(["linux/amd64", "linux/arm64"]),
help="Target platform (defaults to linux/amd64 for Kubernetes compatibility)",
),
is_public: bool = typer.Option(
False,
"--public",
help="Push as a public Prime image under the primeintellect/ namespace.",
),
):
"""
Build and push a Docker image to Prime Intellect registry.
Expand All @@ -365,6 +370,7 @@ def push_image(
prime images push myapp:v1.0.0
prime images push myapp:latest --context ./app --dockerfile ../docker/Dockerfile.prod
prime images push myapp:v1 --platform linux/arm64
prime images push python:3.11 --public
"""
try:
# Parse image reference
Expand All @@ -382,11 +388,20 @@ def push_image(
)
raise typer.Exit(1)

if is_public and config.team_id:
console.print(
"[red]Error: Public images cannot be pushed from a team context. "
"Run 'prime switch personal' first.[/red]"
)
raise typer.Exit(1)

console.print(
f"[bold blue]Building and pushing image:[/bold blue] {image_name}:{image_tag}"
)
if config.team_id:
console.print(f"[dim]Team: {config.team_id}[/dim]")
if is_public:
console.print("[dim]Visibility: public (primeintellect namespace)[/dim]")
console.print()

# Initialize API client
Expand Down Expand Up @@ -466,6 +481,8 @@ def tar_filter(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
}
if config.team_id:
build_payload["team_id"] = config.team_id
if is_public:
build_payload["is_public"] = True

build_response = client.request(
"POST",
Expand Down
76 changes: 76 additions & 0 deletions packages/prime/tests/test_images_push.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import io
import tarfile
from types import SimpleNamespace

from prime_cli.commands import images as images_cmd
from prime_cli.commands.images import PACKAGED_DOCKERFILE_PATH
from prime_cli.main import app
from typer.testing import CliRunner
Expand Down Expand Up @@ -59,6 +61,7 @@ def fake_put(url, content, headers, timeout):

assert result.exit_code == 0, result.output
assert captured["build_payload"]["dockerfile_path"] == PACKAGED_DOCKERFILE_PATH
assert "is_public" not in captured["build_payload"]

with tarfile.open(fileobj=io.BytesIO(captured["tar_bytes"]), mode="r:gz") as tar:
names = set(tar.getnames())
Expand Down Expand Up @@ -138,3 +141,76 @@ def fake_put(url, content, headers, timeout):
dockerfile_member = tar.extractfile(PACKAGED_DOCKERFILE_PATH)
assert dockerfile_member is not None
assert dockerfile_member.read().decode() == dockerfile_path.read_text()


def test_push_image_public_forwards_is_public(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
monkeypatch.setattr("prime_cli.main.check_for_update", lambda: (False, None))
monkeypatch.setattr(images_cmd, "config", SimpleNamespace(team_id=None))

context_path = tmp_path / "context"
context_path.mkdir()
(context_path / "Dockerfile").write_text("FROM busybox\n")

captured = {}

class DummyAPIClient:
def request(self, method, path, json=None, params=None):
if method == "POST" and path == "/images/build":
captured["build_payload"] = json
return {
"build_id": "build-123",
"upload_url": "https://example.test/upload",
"fullImagePath": "primeintellect/rehl:latest",
}

if method == "POST" and path == "/images/build/build-123/start":
return {}

raise AssertionError(f"Unexpected request: {method} {path}")

class DummyUploadResponse:
def raise_for_status(self):
return None

def fake_put(url, content, headers, timeout):
content.read()
return DummyUploadResponse()

monkeypatch.setattr("prime_cli.commands.images.APIClient", DummyAPIClient)
monkeypatch.setattr("prime_cli.commands.images.httpx.put", fake_put)

result = runner.invoke(
app,
["images", "push", "rehl:latest", "--context", "context", "--public"],
env=TEST_ENV,
)

assert result.exit_code == 0, result.output
assert captured["build_payload"]["is_public"] is True
assert "team_id" not in captured["build_payload"]
assert "primeintellect/rehl:latest" in result.output


def test_push_image_public_rejects_team_context(monkeypatch):
monkeypatch.setattr("prime_cli.main.check_for_update", lambda: (False, None))
monkeypatch.setattr(images_cmd, "config", SimpleNamespace(team_id="team-123"))

class DummyAPIClient:
def request(self, method, path, json=None, params=None):
raise AssertionError("API should not be called")

def fake_put(url, content, headers, timeout):
raise AssertionError("Upload should not be called")

monkeypatch.setattr("prime_cli.commands.images.APIClient", DummyAPIClient)
monkeypatch.setattr("prime_cli.commands.images.httpx.put", fake_put)

result = runner.invoke(
app,
["images", "push", "rehl:latest", "--public"],
env=TEST_ENV,
)

assert result.exit_code == 1, result.output
assert "Public images cannot be pushed from a team context" in result.output