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.
- comfygit_deploy/__init__.py +3 -0
- comfygit_deploy/cli.py +374 -0
- comfygit_deploy/commands/__init__.py +5 -0
- comfygit_deploy/commands/custom.py +218 -0
- comfygit_deploy/commands/dev.py +356 -0
- comfygit_deploy/commands/instances.py +506 -0
- comfygit_deploy/commands/runpod.py +203 -0
- comfygit_deploy/commands/worker.py +266 -0
- comfygit_deploy/config.py +122 -0
- comfygit_deploy/providers/__init__.py +11 -0
- comfygit_deploy/providers/custom.py +238 -0
- comfygit_deploy/providers/runpod.py +549 -0
- comfygit_deploy/startup/__init__.py +1 -0
- comfygit_deploy/startup/scripts.py +210 -0
- comfygit_deploy/worker/__init__.py +12 -0
- comfygit_deploy/worker/mdns.py +154 -0
- comfygit_deploy/worker/native_manager.py +438 -0
- comfygit_deploy/worker/server.py +511 -0
- comfygit_deploy/worker/state.py +268 -0
- comfygit_deploy-0.3.4.dist-info/METADATA +38 -0
- comfygit_deploy-0.3.4.dist-info/RECORD +23 -0
- comfygit_deploy-0.3.4.dist-info/WHEEL +4 -0
- comfygit_deploy-0.3.4.dist-info/entry_points.txt +2 -0
|
@@ -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
|