192 lines
7.5 KiB
Python
192 lines
7.5 KiB
Python
#!/usr/bin/env python
|
|
"""Release preflight checks - no ComfyUI required.
|
|
|
|
Run from anywhere: python scripts/release_preflight.py
|
|
|
|
Checks:
|
|
1. pyproject.toml has no UTF-8 BOM. tomllib rejects a BOM, and both
|
|
ComfyUI-Manager and the Comfy Registry parse the file with it, so a
|
|
BOM breaks installs silently.
|
|
2. AUSBOSS_JS_VERSION in js/shared/index.mjs equals the pyproject
|
|
version, so the stale-browser warning never fires on a fresh install.
|
|
3. The README release badge is the DYNAMIC shields.io kind - it reads
|
|
the version out of pyproject.toml on main at view time, so it can
|
|
never go stale (a hardcoded badge sat at 1.0.0 through two releases).
|
|
The check guards against someone swapping a static badge back in.
|
|
4. .comfyignore keeps development-only paths (tests, scripts, CI, agent
|
|
instructions, root docs) out of the Registry archive and never
|
|
swallows a runtime path (nodes/, js/ including js/docs/, the example
|
|
workflows, README, LICENSE, pyproject). comfy-cli's packer honours
|
|
the file with gitignore semantics; this check reads it the same way.
|
|
5. example_workflows/: every UI graph has a matching thumbnail, consistent
|
|
links, setup instructions, and stage groups containing its nodes without
|
|
overlaps (including title bars).
|
|
|
|
Exit code 0 = ready to release, 1 = problems printed below.
|
|
"""
|
|
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
from workflow_contract import example_problems
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
errors = []
|
|
|
|
# --- 1. pyproject.toml is BOM-free and carries a version ---------------------
|
|
pyproject_version = None
|
|
try:
|
|
raw = (ROOT / "pyproject.toml").read_bytes()
|
|
except OSError as exc:
|
|
errors.append(f"could not read pyproject.toml: {exc}")
|
|
raw = b""
|
|
|
|
if raw.startswith(b"\xef\xbb\xbf"):
|
|
errors.append("pyproject.toml starts with a UTF-8 BOM; save it without one")
|
|
|
|
match = re.search(rb'^version\s*=\s*"([^"]+)"', raw, re.MULTILINE)
|
|
if match:
|
|
pyproject_version = match.group(1).decode("ascii", "replace")
|
|
else:
|
|
errors.append('pyproject.toml has no version = "..." line')
|
|
|
|
# --- 2. the frontend version constant matches --------------------------------
|
|
js_version = None
|
|
try:
|
|
shared = (ROOT / "js" / "shared" / "index.mjs").read_text(encoding="utf-8")
|
|
js_match = re.search(r'AUSBOSS_JS_VERSION\s*=\s*"([^"]+)"', shared)
|
|
if js_match:
|
|
js_version = js_match.group(1)
|
|
else:
|
|
errors.append("js/shared/index.mjs does not define AUSBOSS_JS_VERSION")
|
|
except OSError as exc:
|
|
errors.append(f"could not read js/shared/index.mjs: {exc}")
|
|
|
|
if pyproject_version and js_version and pyproject_version != js_version:
|
|
errors.append(
|
|
f"version mismatch: pyproject.toml says {pyproject_version} but "
|
|
f"AUSBOSS_JS_VERSION is {js_version}"
|
|
)
|
|
|
|
# --- 3. the README release badge stays dynamic -------------------------------
|
|
DYNAMIC_BADGE = (
|
|
"img.shields.io/badge/dynamic/toml"
|
|
"?url=https%3A%2F%2Fraw.githubusercontent.com%2Fausboss%2FComfyUI-AusBoss"
|
|
"%2Fmain%2Fpyproject.toml&query=%24.project.version"
|
|
)
|
|
try:
|
|
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
|
if DYNAMIC_BADGE not in readme:
|
|
errors.append(
|
|
"README.md release badge is not the dynamic pyproject-reading "
|
|
"shields.io badge; a hardcoded version WILL go stale"
|
|
)
|
|
if re.search(r"badge/release-\d+\.\d+\.\d+-", readme):
|
|
errors.append(
|
|
"README.md carries a hardcoded release-X.Y.Z badge; use the "
|
|
"dynamic badge only"
|
|
)
|
|
except OSError as exc:
|
|
errors.append(f"could not read README.md: {exc}")
|
|
|
|
# --- 4. .comfyignore covers the development-only paths -----------------------
|
|
import fnmatch
|
|
|
|
|
|
def comfyignore_patterns(text):
|
|
patterns = []
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
patterns.append(line)
|
|
return patterns
|
|
|
|
|
|
def ignored(path, patterns):
|
|
"""gitignore-style match, the subset .comfyignore uses: a trailing slash
|
|
names a directory anywhere in the tree, a leading slash anchors to the
|
|
root, anything else matches a path segment or the full path."""
|
|
parts = path.split("/")
|
|
for pattern in patterns:
|
|
anchored = pattern.startswith("/")
|
|
body = pattern.lstrip("/")
|
|
directory = body.endswith("/")
|
|
body = body.rstrip("/")
|
|
if anchored:
|
|
if directory and parts[0] == body and len(parts) > 1:
|
|
return True
|
|
if not directory and fnmatch.fnmatch(path, body):
|
|
return True
|
|
continue
|
|
if directory:
|
|
if body in parts[:-1]:
|
|
return True
|
|
continue
|
|
if fnmatch.fnmatch(path, body) or any(fnmatch.fnmatch(part, body) for part in parts):
|
|
return True
|
|
return False
|
|
|
|
|
|
DEV_ONLY = [
|
|
"tests/test_math_helpers.py", "tests/panel_guards.test.mjs",
|
|
"scripts/validate_nodes.py", "scripts/release_preflight.py",
|
|
".github/workflows/publish_action.yml", "AGENTS.md", "CLAUDE.md",
|
|
".claude/skills/ausboss-node-brand/SKILL.md", ".agents/skills/ausboss-node-brand/SKILL.md",
|
|
"docs/adding_a_node.md", "todo.md", "todo", "workflow_ideas.md",
|
|
]
|
|
RUNTIME = [
|
|
"__init__.py", "nodes/node_seed.py", "nodes/_lora_helpers.py",
|
|
"js/seed/index.js", "js/shared/index.mjs", "js/docs/AUSBOSS_NODES_Seed.md",
|
|
"example_workflows/Krea 2 Outpaint (AusBoss).json",
|
|
"example_workflows/inputs/ausboss_pier_sunrise.png",
|
|
"README.md", "LICENSE", "pyproject.toml", "ausboss_presets_example.json",
|
|
]
|
|
try:
|
|
patterns = comfyignore_patterns((ROOT / ".comfyignore").read_text(encoding="utf-8"))
|
|
except OSError as exc:
|
|
patterns = None
|
|
errors.append(f"could not read .comfyignore: {exc}")
|
|
if patterns is not None:
|
|
for path in DEV_ONLY:
|
|
if not ignored(path, patterns):
|
|
errors.append(f".comfyignore does not exclude development-only path {path}")
|
|
for path in RUNTIME:
|
|
if ignored(path, patterns):
|
|
errors.append(f".comfyignore would drop runtime path {path} from the archive")
|
|
|
|
# --- 5. example workflows parse and pair with thumbnails ---------------------
|
|
import json
|
|
|
|
examples = ROOT / "example_workflows"
|
|
if examples.is_dir():
|
|
workflows = {path.stem: path for path in examples.glob("*.json")}
|
|
for stem, path in sorted(workflows.items()):
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as exc:
|
|
errors.append(f"example_workflows/{path.name} is not valid JSON: {exc}")
|
|
continue
|
|
if not isinstance(data, dict) or not isinstance(data.get("nodes"), list) or not data["nodes"]:
|
|
errors.append(
|
|
f"example_workflows/{path.name} is not a UI workflow (no nodes list) - "
|
|
"the template browser needs the saved-workflow format, not API JSON"
|
|
)
|
|
if not (examples / f"{stem}.jpg").is_file():
|
|
errors.append(f"example_workflows/{path.name} has no {stem}.jpg - it shows a blank template card")
|
|
for path in sorted(examples.glob("*.jpg")):
|
|
if path.stem not in workflows:
|
|
errors.append(f"example_workflows/{path.name} has no matching {path.stem}.json")
|
|
errors.extend(example_problems(examples))
|
|
|
|
# --- report ------------------------------------------------------------------
|
|
for error in errors:
|
|
print(f"ERROR {error}")
|
|
|
|
if errors:
|
|
print(f"\n{len(errors)} problem(s) found.")
|
|
sys.exit(1)
|
|
|
|
print(f"OK - release preflight passed for version {pyproject_version}.")
|