Skip to content

Commit f1f65a0

Browse files
committed
Enhance packaging with .comfyignore support 📦
- Introduced .comfyignore file to exclude files from packaging - Updated zip_files function to respect .comfyignore patterns - Added tests to validate .comfyignore functionality
1 parent c674bc8 commit f1f65a0

File tree

4 files changed

+253
-29
lines changed

4 files changed

+253
-29
lines changed

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ requirements.compiled
6161
override.txt
6262
.coverage
6363
coverage.xml
64+

‎DEV_README.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,32 @@ either run `pip install -e .` again (which will reinstall), or manually
5858
uninstall `pip uninstall comfy-cli` and reinstall, or even cleaning your conda
5959
env and reinstalling the package (`pip install -e .`)
6060

61+
## Packaging custom nodes with `.comfyignore`
62+
63+
`comfy node pack` and `comfy node publish` now read an optional `.comfyignore`
64+
file in the project root. The syntax matches `.gitignore` (implemented with
65+
`PathSpec`'s `gitwildmatch` rules), so you can reuse familiar patterns to keep
66+
development-only artifacts out of your published archive.
67+
68+
- Patterns are evaluated against paths relative to the directory you run the
69+
command from (usually the repo root).
70+
- Files required by the pack command itself (e.g. `__init__.py`, `web/*`) are
71+
still forced into the archive even if they match an ignore pattern.
72+
- If no `.comfyignore` is present the command falls back to the original
73+
behavior and zips every git-tracked file.
74+
75+
Example `.comfyignore`:
76+
77+
```gitignore
78+
docs/
79+
frontend/
80+
tests/
81+
*.psd
82+
```
83+
84+
Commit the file alongside your node so teammates and CI pipelines produce the
85+
same trimmed package.
86+
6187
## Adding a new command
6288

6389
- Register it under `comfy_cli/cmdline.py`

‎comfy_cli/file_utils.py‎

Lines changed: 130 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
22
import os
33
import pathlib
4+
import subprocess
45
import zipfile
5-
from typing import Optional
6+
from typing import List, Optional, Union
67

78
import httpx
89
import requests
10+
from pathspec import PathSpec
911

1012
from comfy_cli import constants, ui
1113

@@ -86,61 +88,160 @@ def download_file(url: str, local_filepath: pathlib.Path, headers: Optional[dict
8688
raise DownloadException(f"Failed to download file.\n{status_reason}")
8789

8890

91+
def _load_comfyignore_spec(ignore_filename: str = ".comfyignore") -> Optional[PathSpec]:
92+
if not os.path.exists(ignore_filename):
93+
return None
94+
try:
95+
with open(ignore_filename, "r", encoding="utf-8") as ignore_file:
96+
patterns = [
97+
line.strip()
98+
for line in ignore_file
99+
if line.strip() and not line.lstrip().startswith("#")
100+
]
101+
except OSError:
102+
return None
103+
104+
if not patterns:
105+
return None
106+
107+
return PathSpec.from_lines("gitwildmatch", patterns)
108+
109+
110+
def list_git_tracked_files(base_path: Union[str, os.PathLike] = ".") -> List[str]:
111+
try:
112+
result = subprocess.check_output(
113+
["git", "-C", os.fspath(base_path), "ls-files"],
114+
text=True,
115+
)
116+
except (subprocess.SubprocessError, FileNotFoundError):
117+
return []
118+
119+
return [line for line in result.splitlines() if line.strip()]
120+
121+
122+
def _normalize_path(path: str) -> str:
123+
rel_path = os.path.relpath(path, start=".")
124+
if rel_path == ".":
125+
return ""
126+
return rel_path.replace("\\", "/")
127+
128+
129+
def _is_force_included(rel_path: str, include_prefixes: List[str]) -> bool:
130+
return any(
131+
rel_path == prefix or rel_path.startswith(prefix + "/")
132+
for prefix in include_prefixes
133+
if prefix
134+
)
135+
136+
89137
def zip_files(zip_filename, includes=None):
90-
"""
91-
Zip all files in the current directory that are tracked by git,
92-
plus any additional directories specified in includes.
93-
"""
138+
"""Zip git-tracked files respecting optional .comfyignore patterns."""
94139
includes = includes or []
95-
included_paths = set()
96-
git_files = []
140+
include_prefixes: List[str] = [
141+
_normalize_path(os.path.normpath(include.lstrip("/")))
142+
for include in includes
143+
]
97144

98-
try:
99-
import subprocess
145+
included_paths: set[str] = set()
146+
git_files: list[str] = []
100147

101-
git_files = subprocess.check_output(["git", "ls-files"], text=True).splitlines()
102-
except (subprocess.SubprocessError, FileNotFoundError):
148+
ignore_spec = _load_comfyignore_spec()
149+
150+
def should_ignore(rel_path: str) -> bool:
151+
if not ignore_spec:
152+
return False
153+
if _is_force_included(rel_path, include_prefixes):
154+
return False
155+
return ignore_spec.match_file(rel_path)
156+
157+
zip_target = os.fspath(zip_filename)
158+
zip_abs_path = os.path.abspath(zip_target)
159+
zip_basename = os.path.basename(zip_abs_path)
160+
161+
git_files = list_git_tracked_files(".")
162+
if not git_files:
103163
print("Warning: Not in a git repository or git not installed. Zipping all files.")
104164

105-
# Zip only git-tracked files
106-
with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
165+
with zipfile.ZipFile(zip_target, "w", zipfile.ZIP_DEFLATED) as zipf:
107166
if git_files:
108167
for file_path in git_files:
109-
if zip_filename in file_path:
168+
if file_path == zip_basename:
169+
continue
170+
171+
rel_path = _normalize_path(file_path)
172+
if should_ignore(rel_path):
110173
continue
111-
if os.path.exists(file_path):
112-
zipf.write(file_path)
113-
included_paths.add(file_path)
174+
175+
actual_path = os.path.normpath(file_path)
176+
if os.path.abspath(actual_path) == zip_abs_path:
177+
continue
178+
if os.path.exists(actual_path):
179+
arcname = rel_path or os.path.basename(actual_path)
180+
zipf.write(actual_path, arcname)
181+
included_paths.add(rel_path)
114182
else:
115183
print(f"File not found. Not including in zip: {file_path}")
116184
else:
117185
for root, dirs, files in os.walk("."):
118186
if ".git" in dirs:
119187
dirs.remove(".git")
188+
dirs[:] = [
189+
d
190+
for d in dirs
191+
if not should_ignore(_normalize_path(os.path.join(root, d)))
192+
]
120193
for file in files:
121194
file_path = os.path.join(root, file)
122-
# Skip zipping the zip file itself
123-
if zip_filename in file_path:
195+
rel_path = _normalize_path(file_path)
196+
if (
197+
os.path.abspath(file_path) == zip_abs_path
198+
or rel_path in included_paths
199+
or should_ignore(rel_path)
200+
):
124201
continue
125-
relative_path = os.path.relpath(file_path, start=".")
126-
zipf.write(file_path, relative_path)
127-
included_paths.add(file_path)
202+
arcname = rel_path or file_path
203+
zipf.write(file_path, arcname)
204+
included_paths.add(rel_path)
128205

129206
for include_dir in includes:
130-
include_dir = include_dir.lstrip("/")
207+
include_dir = os.path.normpath(include_dir.lstrip("/"))
208+
rel_include = _normalize_path(include_dir)
209+
210+
if os.path.isfile(include_dir):
211+
if not should_ignore(rel_include) and rel_include not in included_paths:
212+
arcname = rel_include or include_dir
213+
zipf.write(include_dir, arcname)
214+
included_paths.add(rel_include)
215+
continue
216+
131217
if not os.path.exists(include_dir):
132-
print(f"Warning: Included directory '{include_dir}' does not exist, creating empty directory")
133-
zipf.writestr(f"{include_dir}/", "")
218+
print(
219+
f"Warning: Included directory '{include_dir}' does not exist, creating empty directory"
220+
)
221+
arcname = rel_include or include_dir
222+
if not arcname.endswith("/"):
223+
arcname = arcname + "/"
224+
zipf.writestr(arcname, "")
134225
continue
135226

136227
for root, dirs, files in os.walk(include_dir):
228+
dirs[:] = [
229+
d
230+
for d in dirs
231+
if not should_ignore(_normalize_path(os.path.join(root, d)))
232+
]
137233
for file in files:
138234
file_path = os.path.join(root, file)
139-
if zip_filename in file_path or file_path in included_paths:
235+
rel_path = _normalize_path(file_path)
236+
if (
237+
os.path.abspath(file_path) == zip_abs_path
238+
or rel_path in included_paths
239+
or should_ignore(rel_path)
240+
):
140241
continue
141-
relative_path = os.path.relpath(file_path, start=".")
142-
zipf.write(file_path, relative_path)
143-
included_paths.add(file_path)
242+
arcname = rel_path or file_path
243+
zipf.write(file_path, arcname)
244+
included_paths.add(rel_path)
144245

145246

146247
def upload_file_to_signed_url(signed_url: str, file_path: str):
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import os
2+
import zipfile
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from comfy_cli import file_utils
8+
9+
10+
@pytest.fixture(autouse=True)
11+
def restore_cwd():
12+
original_cwd = Path.cwd()
13+
try:
14+
yield
15+
finally:
16+
os.chdir(original_cwd)
17+
18+
19+
def test_zip_files_respects_comfyignore(tmp_path, monkeypatch):
20+
project_dir = tmp_path
21+
(project_dir / "keep.txt").write_text("keep", encoding="utf-8")
22+
(project_dir / "ignore.log").write_text("ignore", encoding="utf-8")
23+
ignored_dir = project_dir / "ignored_dir"
24+
ignored_dir.mkdir()
25+
(ignored_dir / "nested.txt").write_text("nested", encoding="utf-8")
26+
27+
(project_dir / ".comfyignore").write_text("*.log\nignored_dir/\n", encoding="utf-8")
28+
29+
zip_path = project_dir / "node.zip"
30+
31+
monkeypatch.chdir(project_dir)
32+
monkeypatch.setattr(
33+
file_utils,
34+
"list_git_tracked_files",
35+
lambda base_path=".": [
36+
"keep.txt",
37+
"ignore.log",
38+
"ignored_dir/nested.txt",
39+
],
40+
)
41+
42+
file_utils.zip_files(str(zip_path))
43+
44+
with zipfile.ZipFile(zip_path, "r") as zf:
45+
names = set(zf.namelist())
46+
47+
assert "keep.txt" in names
48+
assert "ignore.log" not in names
49+
assert not any(name.startswith("ignored_dir/") for name in names)
50+
51+
52+
def test_zip_files_force_include_overrides_ignore(tmp_path, monkeypatch):
53+
project_dir = tmp_path
54+
include_dir = project_dir / "include_me"
55+
include_dir.mkdir()
56+
(include_dir / "data.json").write_text("{}", encoding="utf-8")
57+
58+
(project_dir / "other.txt").write_text("ok", encoding="utf-8")
59+
(project_dir / ".comfyignore").write_text("include_me/\n", encoding="utf-8")
60+
61+
zip_path = project_dir / "node.zip"
62+
63+
monkeypatch.chdir(project_dir)
64+
monkeypatch.setattr(
65+
file_utils,
66+
"list_git_tracked_files",
67+
lambda base_path=".": [
68+
"other.txt",
69+
"include_me/data.json",
70+
],
71+
)
72+
73+
file_utils.zip_files(str(zip_path), includes=["include_me"])
74+
75+
with zipfile.ZipFile(zip_path, "r") as zf:
76+
names = set(zf.namelist())
77+
78+
assert "include_me/data.json" in names
79+
assert "other.txt" in names
80+
81+
82+
def test_zip_files_without_git_falls_back_to_walk(tmp_path, monkeypatch):
83+
project_dir = tmp_path
84+
(project_dir / "file.txt").write_text("data", encoding="utf-8")
85+
zip_path = project_dir / "node.zip"
86+
87+
monkeypatch.chdir(project_dir)
88+
monkeypatch.setattr(file_utils, "list_git_tracked_files", lambda base_path=".": [])
89+
90+
file_utils.zip_files(str(zip_path))
91+
92+
with zipfile.ZipFile(zip_path, "r") as zf:
93+
names = set(zf.namelist())
94+
95+
assert "file.txt" in names
96+
assert "node.zip" not in names

0 commit comments

Comments
 (0)