comfygit-deploy 0.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ """Startup script generator for RunPod deployments.
2
+
3
+ Generates bash scripts that set up ComfyGit environments on RunPod pods
4
+ using the cg CLI commands.
5
+ """
6
+
7
+ import re
8
+ import secrets
9
+ from datetime import datetime
10
+
11
+
12
+ def generate_deployment_id(env_name: str) -> str:
13
+ """Generate a unique deployment ID.
14
+
15
+ Format: deploy-{sanitized_env_name}-{YYYYMMDD}-{HHMMSS}-{random}
16
+
17
+ Args:
18
+ env_name: Source environment name
19
+
20
+ Returns:
21
+ Unique deployment identifier
22
+ """
23
+ # Sanitize env name: replace special chars with dashes, collapse multiple dashes
24
+ sanitized = re.sub(r"[^a-zA-Z0-9-]", "-", env_name)
25
+ sanitized = re.sub(r"-+", "-", sanitized).strip("-").lower()
26
+ if not sanitized:
27
+ sanitized = "env"
28
+
29
+ now = datetime.now()
30
+ timestamp = now.strftime("%Y%m%d-%H%M%S")
31
+ random_suffix = secrets.token_hex(2) # 4 hex chars
32
+
33
+ return f"deploy-{sanitized}-{timestamp}-{random_suffix}"
34
+
35
+
36
+ def generate_startup_script(
37
+ deployment_id: str,
38
+ import_source: str,
39
+ branch: str | None = None,
40
+ comfyui_port: int = 8188,
41
+ ) -> str:
42
+ """Generate the pod startup script for ComfyGit environment setup.
43
+
44
+ This script runs on the RunPod pod and:
45
+ 1. Sets up COMFYGIT_HOME environment variable
46
+ 2. Initializes workspace with cg init --yes
47
+ 3. Imports environment with cg import --yes --name {deployment_id}
48
+ 4. Starts ComfyUI with cg -e {deployment_id} run --listen 0.0.0.0
49
+
50
+ Args:
51
+ deployment_id: Unique identifier for this deployment
52
+ import_source: Git URL or local path for cg import
53
+ branch: Optional git branch/tag for import
54
+ comfyui_port: Port for ComfyUI (default 8188)
55
+
56
+ Returns:
57
+ Bash script string
58
+ """
59
+ # Build branch flag if specified
60
+ branch_flag = f" -b {branch}" if branch else ""
61
+
62
+ return f'''#!/bin/bash
63
+ set -e
64
+
65
+ # =============================================================================
66
+ # ComfyGit Deployment Startup Script
67
+ # Deployment ID: {deployment_id}
68
+ # =============================================================================
69
+
70
+ # Status tracking
71
+ STATUS_FILE="/workspace/.comfygit_deploy_status.json"
72
+ START_TIME=$(date -Iseconds)
73
+
74
+ update_status() {{
75
+ local phase="$1"
76
+ local detail="$2"
77
+ local progress="$3"
78
+ cat > "$STATUS_FILE" << EOF
79
+ {{
80
+ "deployment_id": "{deployment_id}",
81
+ "phase": "$phase",
82
+ "phase_detail": "$detail",
83
+ "progress_percent": $progress,
84
+ "started_at": "$START_TIME",
85
+ "error": null
86
+ }}
87
+ EOF
88
+ }}
89
+
90
+ set_error() {{
91
+ local message="$1"
92
+ cat > "$STATUS_FILE" << EOF
93
+ {{
94
+ "deployment_id": "{deployment_id}",
95
+ "phase": "ERROR",
96
+ "phase_detail": "$message",
97
+ "progress_percent": 0,
98
+ "started_at": "$START_TIME",
99
+ "error": "$message"
100
+ }}
101
+ EOF
102
+ echo "ERROR: $message" >&2
103
+ exit 1
104
+ }}
105
+
106
+ # Ensure workspace directory exists
107
+ mkdir -p /workspace
108
+
109
+ # =============================================================================
110
+ # Phase: INITIALIZING
111
+ # =============================================================================
112
+ update_status "INITIALIZING" "Setting up environment..." 5
113
+
114
+ # Set ComfyGit home directory
115
+ export COMFYGIT_HOME=/workspace/comfygit
116
+ export PATH="$HOME/.local/bin:$PATH"
117
+
118
+ update_status "INITIALIZING" "Installing uv package manager..." 10
119
+
120
+ # Install uv if not present (default RunPod template doesn't include it)
121
+ if ! command -v uv &> /dev/null; then
122
+ curl -LsSf https://astral.sh/uv/install.sh | sh || set_error "Failed to install uv"
123
+ fi
124
+
125
+ # Source uv's env file to ensure PATH is configured correctly
126
+ source "$HOME/.local/bin/env" 2>/dev/null || true
127
+ export PATH="$HOME/.local/bin:$PATH"
128
+
129
+ update_status "INITIALIZING" "Installing ComfyGit CLI..." 12
130
+
131
+ # Install comfygit CLI via uv
132
+ if ! command -v cg &> /dev/null; then
133
+ uv tool install comfygit || set_error "Failed to install comfygit CLI"
134
+ fi
135
+
136
+ # Verify cg is available after installation (catch PATH issues early)
137
+ command -v cg &> /dev/null || set_error "cg command not found after installation - check PATH"
138
+
139
+ # =============================================================================
140
+ # Phase: WORKSPACE_INIT
141
+ # =============================================================================
142
+ update_status "WORKSPACE_INIT" "Initializing ComfyGit workspace..." 15
143
+
144
+ # Initialize workspace (idempotent - fails gracefully if exists)
145
+ mkdir -p "$COMFYGIT_HOME"
146
+ cd "$COMFYGIT_HOME"
147
+ cg init --yes "$COMFYGIT_HOME" 2>/dev/null || true
148
+
149
+ # =============================================================================
150
+ # Phase: ENVIRONMENT_CHECK (handles restart vs fresh deploy)
151
+ # =============================================================================
152
+ ENV_PATH="$COMFYGIT_HOME/environments/{deployment_id}"
153
+
154
+ if [ -d "$ENV_PATH" ] && [ -d "$ENV_PATH/ComfyUI" ]; then
155
+ # Restart case: environment already exists from previous run
156
+ update_status "RESTARTING" "Environment exists, skipping import..." 60
157
+ echo "Detected existing environment: {deployment_id}"
158
+ echo " Skipping import, proceeding to start ComfyUI..."
159
+ cg use {deployment_id} 2>/dev/null || true
160
+ else
161
+ # Fresh deploy: import the environment
162
+ update_status "IMPORTING" "Preparing import source..." 25
163
+
164
+ IMPORT_SOURCE="{import_source}"
165
+
166
+ update_status "IMPORTING" "Importing environment {deployment_id}..." 30
167
+
168
+ # Import the environment (--models all downloads all models with sources)
169
+ cd "$COMFYGIT_HOME"
170
+ cg import "$IMPORT_SOURCE"{branch_flag} --name {deployment_id} --yes --use --models all || set_error "Failed to import environment"
171
+
172
+ update_status "IMPORTING" "Environment imported successfully" 60
173
+ fi
174
+
175
+ # =============================================================================
176
+ # Phase: STARTING_COMFYUI
177
+ # =============================================================================
178
+ update_status "STARTING_COMFYUI" "Starting ComfyUI server..." 80
179
+
180
+ # Start ComfyUI with the imported environment
181
+ cd "$COMFYGIT_HOME"
182
+ nohup cg -e {deployment_id} run --listen 0.0.0.0 --port {comfyui_port} > /workspace/comfyui.log 2>&1 &
183
+ COMFYUI_PID=$!
184
+
185
+ # Wait for ComfyUI to start (check port)
186
+ update_status "STARTING_COMFYUI" "Waiting for ComfyUI to become ready..." 90
187
+
188
+ for i in {{1..120}}; do
189
+ if curl -s http://localhost:{comfyui_port} > /dev/null 2>&1; then
190
+ break
191
+ fi
192
+ sleep 2
193
+ done
194
+
195
+ if ! curl -s http://localhost:{comfyui_port} > /dev/null 2>&1; then
196
+ set_error "ComfyUI failed to start within 4 minutes. Check /workspace/comfyui.log"
197
+ fi
198
+
199
+ # =============================================================================
200
+ # Phase: READY
201
+ # =============================================================================
202
+ update_status "READY" "ComfyUI is running" 100
203
+
204
+ echo "=== Deployment Complete ==="
205
+ echo "Deployment ID: {deployment_id}"
206
+ echo "ComfyUI is running at http://localhost:{comfyui_port}"
207
+
208
+ # Keep the script running (RunPod terminates if script exits)
209
+ tail -f /workspace/comfyui.log
210
+ '''
@@ -0,0 +1,12 @@
1
+ """Worker server components for self-hosted deployment."""
2
+
3
+ from .server import WorkerServer, create_worker_app
4
+ from .state import InstanceState, PortAllocator, WorkerState
5
+
6
+ __all__ = [
7
+ "InstanceState",
8
+ "PortAllocator",
9
+ "WorkerState",
10
+ "WorkerServer",
11
+ "create_worker_app",
12
+ ]
@@ -0,0 +1,154 @@
1
+ """mDNS service broadcasting and scanning for worker discovery.
2
+
3
+ Registers the worker as a _cg-deploy._tcp.local. service so it can be
4
+ discovered by frontends scanning the local network. Also provides client-side
5
+ scanning to discover workers on the network.
6
+ """
7
+
8
+ import socket
9
+ import time
10
+ from dataclasses import dataclass
11
+
12
+ from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf
13
+
14
+ from .. import __version__
15
+
16
+ SERVICE_TYPE = "_cg-deploy._tcp.local."
17
+
18
+
19
+ @dataclass
20
+ class DiscoveredWorker:
21
+ """Worker discovered via mDNS scan."""
22
+
23
+ name: str
24
+ host: str
25
+ port: int
26
+ version: str = "unknown"
27
+ mode: str = "docker"
28
+
29
+
30
+ def get_local_ip() -> str:
31
+ """Get the local IP address of this machine.
32
+
33
+ Returns:
34
+ Local IP address as string, or 127.0.0.1 if detection fails.
35
+ """
36
+ try:
37
+ # Create a socket to determine local IP
38
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
39
+ s.connect(("8.8.8.8", 80))
40
+ ip = s.getsockname()[0]
41
+ s.close()
42
+ return ip
43
+ except Exception:
44
+ return "127.0.0.1"
45
+
46
+
47
+ class MDNSBroadcaster:
48
+ """Broadcasts worker availability via mDNS/Zeroconf."""
49
+
50
+ def __init__(self, port: int, worker_name: str | None = None, mode: str = "docker"):
51
+ """Initialize broadcaster.
52
+
53
+ Args:
54
+ port: Port the worker HTTP server is listening on
55
+ worker_name: Optional name for the worker (defaults to hostname)
56
+ mode: Worker mode (docker or native)
57
+ """
58
+ self.port = port
59
+ self.worker_name = worker_name or socket.gethostname()
60
+ self.mode = mode
61
+ self.zeroconf: Zeroconf | None = None
62
+ self.service_info: ServiceInfo | None = None
63
+
64
+ def start(self) -> None:
65
+ """Register the mDNS service."""
66
+ local_ip = get_local_ip()
67
+
68
+ self.service_info = ServiceInfo(
69
+ SERVICE_TYPE,
70
+ f"{self.worker_name}.{SERVICE_TYPE}",
71
+ addresses=[socket.inet_aton(local_ip)],
72
+ port=self.port,
73
+ properties={
74
+ "version": __version__,
75
+ "name": self.worker_name,
76
+ "mode": self.mode,
77
+ },
78
+ )
79
+
80
+ self.zeroconf = Zeroconf()
81
+ self.zeroconf.register_service(self.service_info)
82
+ print(f" mDNS: Broadcasting as {self.worker_name} on {local_ip}:{self.port}")
83
+
84
+ def stop(self) -> None:
85
+ """Unregister the mDNS service."""
86
+ if self.zeroconf and self.service_info:
87
+ self.zeroconf.unregister_service(self.service_info)
88
+ self.zeroconf.close()
89
+ self.zeroconf = None
90
+ self.service_info = None
91
+
92
+
93
+ class MDNSScanner:
94
+ """Scans the network for ComfyGit workers via mDNS/Zeroconf."""
95
+
96
+ def __init__(self, timeout: float = 5.0):
97
+ """Initialize scanner.
98
+
99
+ Args:
100
+ timeout: How long to scan (seconds)
101
+ """
102
+ self.timeout = timeout
103
+ self._discovered: list[DiscoveredWorker] = []
104
+ self._zeroconf: Zeroconf | None = None
105
+
106
+ def _on_service_state_change(
107
+ self,
108
+ zeroconf: Zeroconf,
109
+ service_type: str,
110
+ name: str,
111
+ state_change: str | ServiceStateChange,
112
+ ) -> None:
113
+ """Called when a service is discovered or removed."""
114
+ # Handle both string and enum state change types
115
+ state = state_change if isinstance(state_change, str) else state_change.name
116
+
117
+ if state in ("Added", "Updated"):
118
+ info = zeroconf.get_service_info(service_type, name)
119
+ if info:
120
+ addresses = info.parsed_addresses()
121
+ if addresses:
122
+ props = info.properties or {}
123
+ worker = DiscoveredWorker(
124
+ name=props.get(b"name", b"unknown").decode(),
125
+ host=addresses[0],
126
+ port=info.port,
127
+ version=props.get(b"version", b"unknown").decode(),
128
+ mode=props.get(b"mode", b"docker").decode(),
129
+ )
130
+ # Avoid duplicates
131
+ if not any(w.host == worker.host and w.port == worker.port for w in self._discovered):
132
+ self._discovered.append(worker)
133
+
134
+ def scan(self) -> list[DiscoveredWorker]:
135
+ """Scan for workers on the network.
136
+
137
+ Returns:
138
+ List of discovered workers
139
+ """
140
+ self._discovered = []
141
+ zeroconf = Zeroconf()
142
+ self._zeroconf = zeroconf
143
+
144
+ try:
145
+ ServiceBrowser(zeroconf, SERVICE_TYPE, handlers=[self._on_service_state_change])
146
+ time.sleep(self.timeout)
147
+ finally:
148
+ zeroconf.close()
149
+
150
+ return self._discovered
151
+
152
+ def get_discovered(self) -> list[DiscoveredWorker]:
153
+ """Get workers discovered so far (for manual callback testing)."""
154
+ return self._discovered