Compare commits

...
Author SHA1 Message Date
ausboss 7bc156a819 Drop an unused import in the second-copy test 2026-11-09 15:23:08 -06:00
ausboss 7a1f89ab15 Skip a second install of the pack instead of stopping ComfyUI
Manager installs the pack in custom_nodes/ausboss-nodes and a git clone
lands in custom_nodes/ComfyUI-AusBoss. With both folders present, both
copies registered GET /ausboss/pack_modules and ComfyUI stopped at
startup: "Added route will never be executed, method HEAD is already
registered".

The first copy to load claims a marker in sys.modules. A later copy
loads no node modules, registers no routes, serves no js folder, and
prints which folder it skipped.
2026-11-09 15:21:25 -06:00
ausboss 146e3cfa8b Merge pull request #322 from ausboss/ci/publish-downloader-note
Publish Model Downloader: take the release note as an input
2026-11-09 00:35:55 -06:00
ausboss e22ddb90d5 Publish Model Downloader: take the release note as an input
The job carried the 0.1.0 note as a fixed string, so every later
version would have shown "Initial release" on the Registry. The note is
now a required input of the manual run, and the publish step refuses a
blank one.
2026-11-09 00:34:40 -06:00
5 changed files with 179 additions and 6 deletions
@@ -7,6 +7,10 @@ on:
description: Full commit SHA from ComfyUI-AusBoss-Model-Downloader main
required: true
type: string
changelog:
description: Release note for this version, shown in the Registry's Updates section
required: true
type: string
# The existing AusBoss publisher credential stays in this repository. This
# manual job publishes only the separately maintained Model Downloader addon.
@@ -62,12 +66,16 @@ jobs:
id: publish
env:
REGISTRY_ACCESS_TOKEN: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
COMFY_NODE_CHANGELOG: 'Initial release: model downloads from Civitai and Hugging Face, installed-file checks, saved model lists, and AusBoss Workflow Note integration.'
COMFY_NODE_CHANGELOG: ${{ inputs.changelog }}
run: |
if [ -z "$REGISTRY_ACCESS_TOKEN" ]; then
echo '::error::REGISTRY_ACCESS_TOKEN is not configured.'
exit 1
fi
if [ -z "${COMFY_NODE_CHANGELOG// /}" ]; then
echo '::error::The changelog input is empty; the Registry shows it as the release note.'
exit 1
fi
comfy --skip-prompt --no-enable-telemetry node publish --token "$REGISTRY_ACCESS_TOKEN"
- name: Report Registry review status
if: always() && steps.publish.outcome != 'skipped'
+7
View File
@@ -2,6 +2,13 @@
All notable changes to ComfyUI-AusBoss are documented here.
## Unreleased
- **ComfyUI starts when the pack is installed twice.** Manager installs it in
`custom_nodes/ausboss-nodes` and a git clone in `custom_nodes/ComfyUI-AusBoss`.
With both there, ComfyUI used to stop at startup. Now the second copy loads
nothing, and the console names both folders so you can delete one.
## 2.23.0 - 2026-11-09
- **Mask by Touch keeps your picks when the picture changes.** Your clicks
+2
View File
@@ -42,6 +42,8 @@ git clone https://github.com/ausboss/ComfyUI-AusBoss.git
Restart ComfyUI and search the node library for **AusBoss**.
Install it one way, not both. Manager puts the pack in `custom_nodes/ausboss-nodes` and a clone in `custom_nodes/ComfyUI-AusBoss`. They are the same pack, so if both folders are there, delete one. The console says which copy it skipped.
After an update, through Manager or `git pull`, restart ComfyUI and hard-refresh the browser with **Ctrl+Shift+R**. Otherwise the browser keeps the old JavaScript, and the pack warns you about it. Manager can install a version or two behind while the Comfy Registry reviews a new release. To get the newest one, pick it in Manager's version list, or update by hand: delete the pack's folder in `ComfyUI/custom_nodes` and clone the repository as shown above.
The pack uses the Pillow, NumPy, Torch and PyAV that come with ComfyUI. Each example lists the models it needs on its Workflow Note. LaMa Inpaint also needs [`big-lama.pt`](https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt) in `ComfyUI/models/lama/`. The pack never downloads weights by itself.
+33 -5
View File
@@ -7,12 +7,26 @@ name to NODE_MODULES below — nothing else to wire up.
A module that fails to import is skipped with a console warning instead
of taking the whole pack down, so one broken node never hides the rest.
Only one copy of the pack loads. People install it from ComfyUI-Manager
(custom_nodes/ausboss-nodes) and from GitHub (custom_nodes/ComfyUI-AusBoss),
and two copies used to register the same server routes, which stopped
ComfyUI at startup. A second copy now loads nothing and says which folder
to delete.
"""
import importlib
import os
import sys
import tomllib
import traceback
import types
_HERE = os.path.dirname(os.path.abspath(__file__))
_FIRST_COPY = sys.modules.setdefault(
"_ausboss_nodes_first_copy", types.SimpleNamespace(folder=_HERE)
)
_IS_SECOND_COPY = _FIRST_COPY.folder != _HERE
# One entry per file in nodes/, kept alphabetical. The order has no
# functional effect.
@@ -64,6 +78,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
_failed_modules = []
for _module_name in NODE_MODULES:
if _IS_SECOND_COPY:
break
try:
_module = importlib.import_module(f".nodes.{_module_name}", __name__)
NODE_CLASS_MAPPINGS.update(getattr(_module, "NODE_CLASS_MAPPINGS", {}))
@@ -76,15 +92,15 @@ for _module_name in NODE_MODULES:
# Folder ComfyUI serves to the browser; every .js file in it is loaded as a
# frontend extension. Shared modules use .mjs so they are import-only.
WEB_DIRECTORY = "./js"
WEB_DIRECTORY = None if _IS_SECOND_COPY else "./js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
def _read_version():
def _read_version(folder=None):
"""Version straight from pyproject.toml so it is never duplicated."""
try:
pyproject = os.path.join(os.path.dirname(__file__), "pyproject.toml")
pyproject = os.path.join(folder or os.path.dirname(__file__), "pyproject.toml")
with open(pyproject, "rb") as f:
version = tomllib.load(f).get("project", {}).get("version")
return version if isinstance(version, str) and version.strip() else "unknown"
@@ -102,6 +118,17 @@ def _print_banner():
bar = f"{teal}{'-' * 70}{reset}"
print(bar)
if _IS_SECOND_COPY:
def where(folder):
text = f"{os.path.basename(folder)} (v{_read_version(folder)})"
return text.encode("ascii", "replace").decode("ascii")
print(f" {red}AusBoss is installed twice, so this copy is not loaded.{reset}")
print(f" In use: custom_nodes/{where(_FIRST_COPY.folder)}")
print(f" Skipped: custom_nodes/{where(_HERE)}")
print(" Delete one of the two folders and restart ComfyUI.")
print(bar)
return
print(
f" {teal}AusBoss{reset} v{_read_version()} | "
f"{len(NODE_CLASS_MAPPINGS)} nodes loaded"
@@ -113,9 +140,10 @@ def _print_banner():
try:
from .nodes._frontend_cache import register_frontend_cache_guard
if not _IS_SECOND_COPY:
from .nodes._frontend_cache import register_frontend_cache_guard
register_frontend_cache_guard()
register_frontend_cache_guard()
except Exception:
traceback.print_exc()
+128
View File
@@ -0,0 +1,128 @@
"""A second install of the pack loads nothing, so ComfyUI still starts.
People install the pack from ComfyUI-Manager (custom_nodes/ausboss-nodes) and
from GitHub (custom_nodes/ComfyUI-AusBoss). With both folders present, the two
copies registered the same server routes, and ComfyUI stopped at startup with
"Added route will never be executed, method HEAD is already registered".
This loads the pack twice, from two folders, in one fresh standard-library
Python with the same stand-ins as tests/test_node_api.py. The second copy must
add no routes, no nodes and no browser files, and must name both folders.
"""
from __future__ import annotations
from pathlib import Path
import contextlib
import importlib.util
import io
import json
import subprocess
import sys
import tempfile
import types
import unittest
ROOT = Path(__file__).resolve().parent.parent
ROUTES: list[tuple[str, str]] = []
def stub_server():
"""A PromptServer whose route table records every route, the way aiohttp will add them."""
class Routes:
def __getattr__(self, method):
def register(path, *_args, **_kwargs):
ROUTES.append((method.upper(), path))
return lambda handler: handler
return register
class Instance:
routes = Routes()
def __getattr__(self, _name):
return lambda *_args, **_kwargs: None
module = types.ModuleType("server")
module.PromptServer = type("PromptServer", (), {"instance": Instance()})
sys.modules["server"] = module
def load_copy(name: str, folder: Path):
spec = importlib.util.spec_from_file_location(
name, folder / "__init__.py", submodule_search_locations=[str(folder)]
)
pack = importlib.util.module_from_spec(spec)
sys.modules[name] = pack
out = io.StringIO()
with contextlib.redirect_stdout(out):
spec.loader.exec_module(pack)
return pack, out.getvalue()
def child(tmp: Path, report: Path):
sys.path.insert(0, str(ROOT / "tests"))
from test_node_api import StandInFinder, StandInModule
sys.meta_path.insert(0, StandInFinder())
sys.modules["aiohttp"] = StandInModule("aiohttp") # route helpers import aiohttp.web first
stub_server()
# The two folder names people end up with: a git clone and Manager's install.
first, second = tmp / "ComfyUI-AusBoss", tmp / "ausboss-nodes"
first.symlink_to(ROOT, target_is_directory=True)
second.symlink_to(ROOT, target_is_directory=True)
first_pack, _ = load_copy("ComfyUI-AusBoss", first)
first_routes = list(ROUTES)
second_pack, second_out = load_copy("ausboss-nodes", second)
report.write_text(json.dumps({
"first_nodes": len(first_pack.NODE_CLASS_MAPPINGS),
"first_web": first_pack.WEB_DIRECTORY,
"first_routes": len(first_routes),
"second_nodes": len(second_pack.NODE_CLASS_MAPPINGS),
"second_web": second_pack.WEB_DIRECTORY,
"second_routes": len(ROUTES) - len(first_routes),
"duplicates": sorted({f"{m} {p}" for m, p in ROUTES if ROUTES.count((m, p)) > 1}),
"second_out": second_out,
}), encoding="utf-8")
class SecondCopyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
with tempfile.TemporaryDirectory() as tmp:
report = Path(tmp) / "report.json"
result = subprocess.run(
[sys.executable, "-S", str(Path(__file__).resolve()), "--child", tmp, str(report)],
cwd=ROOT, capture_output=True, text=True, timeout=120,
)
if result.returncode or not report.exists():
raise AssertionError(f"Loading two copies failed:\n{result.stdout}\n{result.stderr}")
cls.found = json.loads(report.read_text(encoding="utf-8"))
def test_first_copy_loads_as_usual(self):
self.assertGreater(self.found["first_nodes"], 0)
self.assertGreater(self.found["first_routes"], 0)
self.assertEqual(self.found["first_web"], "./js")
def test_second_copy_adds_no_routes(self):
self.assertEqual(self.found["duplicates"], [])
self.assertEqual(self.found["second_routes"], 0)
def test_second_copy_adds_no_nodes_or_browser_files(self):
self.assertEqual(self.found["second_nodes"], 0)
self.assertIsNone(self.found["second_web"])
def test_second_copy_names_both_folders(self):
out = self.found["second_out"]
self.assertIn("ComfyUI-AusBoss", out)
self.assertIn("ausboss-nodes", out)
out.encode("ascii") # cp1252 consoles: the message must be plain ASCII
if __name__ == "__main__":
if len(sys.argv) == 4 and sys.argv[1] == "--child":
child(Path(sys.argv[2]), Path(sys.argv[3]))
else:
unittest.main()