From 126d1a8d5dc845a1730532a20faf72be4ce0fc36 Mon Sep 17 00:00:00 2001 From: "yinsu.zs" Date: Thu, 23 Apr 2026 22:28:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(comfyui):=20=E5=8D=87=E7=BA=A7=E5=88=B0=20?= =?UTF-8?q?v0.19.4=20=E5=B9=B6=E6=95=B4=E7=90=86=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E4=B8=8E=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ComfyUI Dockerfile:v0.19.4 源码与依赖安装顺序;内置 custom_nodes 仍复制到 /root/built-in 供 Agent COPY;去掉重装 requirements 的重复 pip。 - Agent Dockerfile:更新默认 COMFYUI_IMAGE;built-in version.txt 写入 1.6.7。 - comfyui Makefile:镜像/OSS 前缀 v0.19.4-alpha;FunArt dependency_version 1.6.7;input 仍从 v0.3.77-alpha 拷贝。 - custom_nodes.json / update_custom_nodes_versions.py / README:插件钉扎与版本脚本说明。 Change-Id: I2e03420e7e523850895599adec7e11e7f31018c7 Co-developed-by: Cursor --- src/code/agent/Dockerfile | 4 +- src/code/comfyui/Dockerfile | 20 +- src/code/comfyui/Makefile | 12 +- .../comfyui/custom_nodes_config/README.md | 24 +++ .../custom_nodes_config/custom_nodes.json | 59 +++--- .../update_custom_nodes_versions.py | 193 ++++++++++++++++++ 6 files changed, 261 insertions(+), 51 deletions(-) create mode 100644 src/code/comfyui/custom_nodes_config/update_custom_nodes_versions.py diff --git a/src/code/agent/Dockerfile b/src/code/agent/Dockerfile index 3a00af48..eaa83055 100644 --- a/src/code/agent/Dockerfile +++ b/src/code/agent/Dockerfile @@ -1,4 +1,4 @@ -ARG COMFYUI_IMAGE=cap-demo-public-registry.cn-hangzhou.cr.aliyuncs.com/aliyunfc/funart-comfyui:v1.6.5 +ARG COMFYUI_IMAGE=cap-demo-public-registry.cn-hangzhou.cr.aliyuncs.com/cap-app/image-generation-comfyui-agent:v1.3.10 FROM ${COMFYUI_IMAGE} AS comfyui_source FROM python:3.10.16-slim @@ -50,7 +50,7 @@ COPY --from=comfyui_source /root/built-in/custom_nodes /root/built-in/custom_nod # 写入内置插件版本号,供启动时版本检查使用 # 版本号需随内置插件列表的实质性变化(增删插件、升级版本)而更新 -RUN echo "1.6.5" > /root/built-in/version.txt +RUN echo "1.6.7" > /root/built-in/version.txt ENV WORK_DIR='/root' ENV COMFYUI_DIR="${WORK_DIR}/comfyui" diff --git a/src/code/comfyui/Dockerfile b/src/code/comfyui/Dockerfile index 855497d8..0989c18a 100644 --- a/src/code/comfyui/Dockerfile +++ b/src/code/comfyui/Dockerfile @@ -9,10 +9,10 @@ ENV COMFYUI_DIR="/root/comfyui" RUN git clone https://github.com/comfyanonymous/ComfyUI.git ${COMFYUI_DIR} && \ cd ${COMFYUI_DIR} && \ - git checkout "v0.3.77" + git checkout "v0.19.4" # 复制插件列表配置文件 -COPY code/comfyui/custom_nodes_config/custom_nodes.json /tmp/custom_nodes.json +COPY comfyui/custom_nodes_config/custom_nodes.json /tmp/custom_nodes.json # 批量安装自定义节点插件 # 从 custom_nodes.json 读取插件列表,循环安装启用的插件 @@ -80,7 +80,7 @@ RUN cd ${COMFYUI_DIR}/custom_nodes && \ fi # 拷贝自研插件(如果有) -COPY code/comfyui/custom_nodes/ ${COMFYUI_DIR}/custom_nodes/ +COPY comfyui/custom_nodes/ ${COMFYUI_DIR}/custom_nodes/ FROM python:3.10.16-slim AS dependencies @@ -116,7 +116,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ # 安装 ComfyUI 插件的依赖 # 使用 ManagementService.install_custom_nodes - 智能合并依赖,处理冲突(需要 agent 代码) ENV AGENT_DIR="/root/agent" -COPY code/agent ${AGENT_DIR} +COPY agent ${AGENT_DIR} # 安装 agent 的依赖(供 ManagementService 运行,使用主 Python 环境) RUN --mount=type=cache,target=/root/.cache/pip \ @@ -132,10 +132,9 @@ RUN --mount=type=cache,target=/root/.cache/pip \ # 重新安装 ComfyUI 依赖以修复被插件修改的版本,并修正特定版本冲突 RUN --mount=type=cache,target=/root/.cache/pip \ - # 重新安装 ComfyUI 依赖 - /root/venv/bin/pip install -r ${COMFYUI_DIR}/requirements.txt --no-cache-dir && \ - # 安装 transformers 依赖, transformers 5.0.0 与 comfyui0.3.77 不兼容 - /root/venv/bin/pip install transformers==4.56.2 --no-cache-dir && \ + # 与官方 requirements 一致使用 cu128 索引,避免 torch 被换成 CPU 版导致后续 wheel 失败 + /root/venv/bin/pip install -r ${COMFYUI_DIR}/requirements.txt --no-cache-dir \ + --extra-index-url https://download.pytorch.org/whl/cu128 && \ /root/venv/bin/pip install setproctitle dill scikit-image -i https://mirrors.bfsu.edu.cn/pypi/web/simple/ && \ # /root/venv/bin/pip install flash_attn && \ # 再次强制安装指定版本的 PyTorch,防止被插件依赖覆盖 @@ -161,7 +160,7 @@ RUN \ git curl wget jq ffmpeg gcc g++ build-essential zstd && \ rm -rf /var/lib/apt/lists/* -COPY code/agent ${AGENT_DIR} +COPY agent ${AGENT_DIR} RUN python3 -m venv ${AGENT_DIR}/venv @@ -187,6 +186,9 @@ COPY --from=dependencies ${VENV_DIR} ${VENV_DIR} COPY --from=codes ${COMFYUI_DIR} ${COMFYUI_DIR} COPY --from=agent ${AGENT_DIR} ${AGENT_DIR} +# 与 aliyunfc/funart-comfyui 约定一致:agent 镜像构建时会 COPY --from=COMFYUI_IMAGE /root/built-in/custom_nodes +RUN mkdir -p /root/built-in && cp -a "${COMFYUI_DIR}/custom_nodes" /root/built-in/custom_nodes + ENV BACKEND_TYPE="comfyui" RUN chmod +x ${AGENT_DIR}/entrypoint.bash diff --git a/src/code/comfyui/Makefile b/src/code/comfyui/Makefile index d68b8fb7..9fe779cd 100644 --- a/src/code/comfyui/Makefile +++ b/src/code/comfyui/Makefile @@ -1,10 +1,10 @@ # 定义变量 REGION ?= cn-hangzhou -COMFYUI_IMAGE = comfyui:v1.6.5 -COMFYUI_DEEPGPU_IMAGE = comfyui:deepgpu-v1.6.5-v1 +COMFYUI_IMAGE = comfyui:v0.19.4 +COMFYUI_DEEPGPU_IMAGE = comfyui:deepgpu-v0.19.4-v1 OSS_BUCKET ?= dipper-cache-$(REGION) -OSS_COMFYUI_BASE_DIR = base/comfyui/v0.3.77-gamma -OSS_COMFYUI_DEEPGPU_BASE_DIR = base/comfyui/v0.3.77-gamma-deepgpu +OSS_COMFYUI_BASE_DIR = base/comfyui/v0.19.4-alpha +OSS_COMFYUI_DEEPGPU_BASE_DIR = base/comfyui/v0.19.4-alpha-deepgpu .PHONY: upgrade upgrade: build upload-base @@ -58,7 +58,7 @@ upload-base: @echo "Creating .funart/VERSION.txt..." @mkdir -p ./tmp/.funart - @echo "1.6.5" > ./tmp/.funart/dependency_version.txt + @echo "1.6.7" > ./tmp/.funart/dependency_version.txt @echo "Uploading files to OSS..." ossutil cp -r ./tmp/models oss://$(OSS_BUCKET)/$(OSS_COMFYUI_BASE_DIR)/models && \ @@ -98,7 +98,7 @@ upload-deepgpu-base: @echo "Creating .funart/VERSION.txt..." @mkdir -p ./tmp/.funart - @echo "1.6.5" > ./tmp/.funart/dependency_version.txt + @echo "1.6.7" > ./tmp/.funart/dependency_version.txt @echo "Uploading files to OSS..." ossutil cp -r ./tmp/custom_nodes oss://$(OSS_BUCKET)/$(OSS_COMFYUI_DEEPGPU_BASE_DIR)/custom_nodes && \ diff --git a/src/code/comfyui/custom_nodes_config/README.md b/src/code/comfyui/custom_nodes_config/README.md index d3e8c6e5..30d5de5e 100644 --- a/src/code/comfyui/custom_nodes_config/README.md +++ b/src/code/comfyui/custom_nodes_config/README.md @@ -9,6 +9,7 @@ | 文件 | 类型 | 用途 | |------|------|------| | `get_topn.py` | 脚本 | 从网络拉取最新数据,生成 Top N 插件列表 | +| `update_custom_nodes_versions.py` | 脚本 | 批量解析各插件仓库的 Git 引用,回写 `custom_nodes.json` 中的 `version` | | `custom_nodes.json` | 配置 | 镜像内置核心插件列表(49 个) | | `excluded_custom_nodes.json` | 配置 | 排除列表,记录不内置的插件及原因 | | `custom_nodes_top100.json` | 数据 | 按 Stars 排序的 Top 100 插件列表 | @@ -135,6 +136,29 @@ cp custom_nodes_config/custom_nodes_top_150_2026-03-05.json \ --- +## update_custom_nodes_versions.py — 批量更新插件 `version` + +用 **`git ls-remote`** 查远程仓库(不克隆),按规则算出新的 `version` 写回 **`custom_nodes.json`**。镜像构建里会对 `version` 做 **`git checkout`**(`latest` / 空串除外),所以跑完脚本等于批量换钉的版本。 + +**当前 `version` 怎么被改写:** + +| 你写的 `version` | 脚本会改成 | +|------------------|------------| +| `latest` 或空 | 远程默认分支最新提交的 **SHA 前 12 位** | +| 其它(commit 前缀、tag 名、任意占位字符串等) | 远程若有 **semver 标签**,则改成 **版本号最大的 tag**;否则改成默认分支 **SHA 前 12 位** | + +说明:配置里只保留 **短 commit** 或 **版本号 tag** 即可;不要写分支名 `main` / `master`,否则也会按上表第二行被解析成 **tag 或 SHA**。 + +依赖:**本机 `git`**、Python **`packaging`**(`pip install packaging`)。默认并行 8 个仓库;有解析失败的条目会跳过并 **退出码 1**。 + +```bash +cd custom_nodes_config +python3 update_custom_nodes_versions.py # 写回 custom_nodes.json +python3 update_custom_nodes_versions.py --dry-run # 只看变更,不写文件 +``` + +--- + ## 相关设计文档 详细的系统插件架构设计请参考:[系统插件设计文档](../../../system-plugins-design.md) diff --git a/src/code/comfyui/custom_nodes_config/custom_nodes.json b/src/code/comfyui/custom_nodes_config/custom_nodes.json index ed450af8..4845a82f 100644 --- a/src/code/comfyui/custom_nodes_config/custom_nodes.json +++ b/src/code/comfyui/custom_nodes_config/custom_nodes.json @@ -1,11 +1,12 @@ { "metadata": { - "generated_at": "2026-03-05", + "generated_at": "2026-04-23", "top_n": 100, "total": 91, "source_stats_url": "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/github-stats.json", "source_list_url": "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/custom-node-list.json", - "excluded_count": 42 + "excluded_count": 42, + "version_resolve_note": "versions resolved by update_custom_nodes_versions.py (prefer latest semver tag, else default-branch SHA prefix)" }, "custom_nodes": [ { @@ -15,7 +16,7 @@ "stars": 13812, "enabled": true, "description": "ComfyUI-Manager itself is also a custom node.", - "version": "3.39" + "version": "3.39.3" }, { "id": "comfyui-wanvideowrapper", @@ -51,7 +52,7 @@ "stars": 3826, "enabled": true, "description": "Plug-and-play ComfyUI node sets for making ControlNet hint images.", - "version": "95a13e2" + "version": "e8b689a" }, { "id": "ad-evolved", @@ -60,7 +61,7 @@ "stars": 3398, "enabled": true, "description": "A forked repository that actively maintains [a/AnimateDiff](https://github.com/ArtVentureX/comfyui-animatediff), created by ArtVentureX.\n\nImproved AnimateDiff integration for ComfyUI, adapts from sd-webui-animatediff.\n[w/Download one or more motion models from [a/Original Models](https://huggingface.co/guoyww/animatediff/tree/main) | [a/Finetuned Models](https://huggingface.co/manshoety/AD_Stabilized_Motion/tree/main). See README for additional model links and usage. Put the model weights under %%ComfyUI/custom_nodes/ComfyUI-AnimateDiff-Evolved/models%%. You are free to rename the models, but keeping original names will ease use when sharing your workflow.]", - "version": "90fb133" + "version": "d8d163c" }, { "id": "comfyui-gguf", @@ -114,7 +115,7 @@ "stars": 2791, "enabled": true, "description": "Nunchaku ComfyUI Node. Nunchaku is the inference that supports SVDQuant. SVDQuant is a new post-training training quantization paradigm for diffusion models, which quantize both the weights and activations of FLUX.1 to 4 bits, achieving 3.5× memory and 8.7× latency reduction on a 16GB laptop 4090 GPU. See more details: https://github.com/mit-han-lab/nunchaku", - "version": "v1.0.2" + "version": "v1.2.1" }, { "id": "advancedliveportrait", @@ -159,7 +160,7 @@ "stars": 2257, "enabled": true, "description": "This custom node allows you to generate pure python code from your ComfyUI workflow with the click of a button. Great for rapid experimentation or production deployment.", - "version": "v1.3.1" + "version": "v2.1.0" }, { "id": "supir", @@ -168,7 +169,7 @@ "stars": 2233, "enabled": true, "description": "Wrapper nodes to use SUPIR upscaling process in ComfyUI", - "version": "0613a92" + "version": "fe0d660" }, { "id": "liveportrait-kijai", @@ -222,7 +223,7 @@ "stars": 1618, "enabled": true, "description": "Nodes to use Florence2 VLM for image vision tasks: object detection, captioning, segmentation and ocr", - "version": "606bc5c" + "version": "d8cb44d" }, { "id": "comfyui-cogvideoxwrapper", @@ -240,7 +241,7 @@ "stars": 1522, "enabled": true, "description": "Nodes related to video workflows", - "version": "993082e" + "version": "2984ec4" }, { "id": "alekpet", @@ -249,7 +250,7 @@ "stars": 1464, "enabled": true, "description": "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode.", - "version": "ba55cc2" + "version": "6310c31" }, { "id": "usdu", @@ -258,7 +259,7 @@ "stars": 1452, "enabled": true, "description": "ComfyUI nodes for the Ultimate Stable Diffusion Upscale script by Coyote-A.", - "version": "34a3c2c" + "version": "bebd569" }, { "id": "eff-nodes", @@ -267,7 +268,7 @@ "stars": 1427, "enabled": true, "description": "A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count.[w/NOTE: This node is originally created by LucianoCirino, but the [a/original repository](https://github.com/LucianoCirino/efficiency-nodes-comfyui) is no longer maintained and has been forked by a new maintainer. To use the forked version, you should uninstall the original version and **REINSTALL** this one.]", - "version": "f0971b5" + "version": "4579b7d" }, { "id": "comfyui-workspace-manager", @@ -276,7 +277,7 @@ "stars": 1417, "enabled": true, "description": "A ComfyUI custom node for project management to centralize the management of all your workflows in one place. Seamlessly switch between workflows, create and update them within a single workspace, like Google Docs.", - "version": "prod" + "version": "dfc9218" }, { "id": "comfyui-fluxtapoz", @@ -368,15 +369,6 @@ "description": "Based on GroundingDino and SAM, use semantic strings to segment any element in an image. The comfyui version of sd-webui-segment-anything.", "version": "ab63955" }, - { - "id": "teacache", - "name": "ComfyUI-TeaCache", - "repository": "https://github.com/welltop-cn/ComfyUI-TeaCache", - "stars": 1066, - "enabled": true, - "description": "Unofficial implementation of [ali-vilab/TeaCache](https://github.com/ali-vilab/TeaCache) for ComfyUI", - "version": "91dff8e" - }, { "id": "essentials", "name": "ComfyUI Essentials", @@ -402,7 +394,7 @@ "stars": 1036, "enabled": true, "description": "Achieve seamless inpainting results without needing a specialized inpainting model.", - "version": "Release" + "version": "1.5.3" }, { "id": "res4lyf", @@ -420,7 +412,7 @@ "stars": 987, "enabled": true, "description": "'✂️ Inpaint Crop' is a node that crops an image before sampling. The context area can be specified via the mask, expand pixels and expand factor or via a separate (optional) mask.\n'✂️ Inpaint Stitch' is a node that stitches the inpainted image back into the original image without altering unmasked areas.", - "version": "c2c2a41" + "version": "8e59ab1" }, { "id": "steerable-motion", @@ -438,7 +430,7 @@ "stars": 951, "enabled": true, "description": "This is a workflow for my simple logic amazing upscale node for DIT model. it can be common use for Flux,Hunyuan,SD3 It can simple tile the initial image into pieces and then use image-interrogator to get each tile prompts for more accurate upscale process. The condition will be properly handled and the hallucination will be significantly eliminated.", - "version": "v1.0.2" + "version": "V1.0.3" }, { "id": "adv-cnet", @@ -447,7 +439,7 @@ "stars": 949, "enabled": true, "description": "Nodes for scheduling ControlNet strength across timesteps and batched latents, as well as applying custom weights and attention masks.", - "version": "2bde95a" + "version": "b03791e" }, { "id": "ue", @@ -456,7 +448,7 @@ "stars": 942, "enabled": true, "description": "A set of nodes that allow data to be 'broadcast' to some or all unconnected inputs. Greatly reduces link spaghetti.", - "version": "c01c3ce" + "version": "a3952a4" }, { "id": "comfyui-latentsyncwrapper", @@ -573,7 +565,7 @@ "stars": 724, "enabled": true, "description": "TTS Audio Suite - Universal multi-engine TTS extension for ComfyUI with unified architecture supporting ChatterBox, F5-TTS, and future engines like RVC. Features modular engine adapters, character voice management, comprehensive SRT subtitle support, and advanced audio processing capabilities.", - "version": "v4.22.0" + "version": "v4.25.1" }, { "id": "lucy-edit-comfyui", @@ -629,7 +621,6 @@ "description": "ComfyUI-QwenVL custom node: Integrates the Qwen-VL series, including Qwen2.5-VL and the latest Qwen3-VL, to enable advanced multimodal AI for text generation, image understanding, and video analysis.", "version": "fcd1ada" }, - { "id": "advancedRefluxControl", "name": "Advanced Reflux control", @@ -664,7 +655,7 @@ "stars": 644, "enabled": true, "description": "NODES: An industrial-grade zero-shot text-to-speech synthesis system with a ComfyUI interface.", - "version": "928812f" + "version": "81f6d05" }, { "id": "external-tooling", @@ -682,7 +673,7 @@ "stars": 623, "enabled": true, "description": "The nodes detached from ComfyUI Layer Style are mainly those with complex requirements for dependency packages.", - "version": "6292ad8" + "version": "7b678b4" }, { "id": "comfyui-enricos-nodes", @@ -799,7 +790,7 @@ "stars": 10, "enabled": true, "description": "常用 ComfyUI 节点集合,包含高级裁剪、图像缩放、批处理、图像拼接等实用节点,源自 ComfyUI_LayerStyle。", - "version": "cb6e904" + "version": "789eff2" } ] -} \ No newline at end of file +} diff --git a/src/code/comfyui/custom_nodes_config/update_custom_nodes_versions.py b/src/code/comfyui/custom_nodes_config/update_custom_nodes_versions.py new file mode 100644 index 00000000..fdcce31c --- /dev/null +++ b/src/code/comfyui/custom_nodes_config/update_custom_nodes_versions.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Resolve each custom_nodes.json entry to a concrete git ref (prefer latest semver tag, +else default-branch tip) and rewrite version fields. + +Usage (from repo root or this directory): + python3 update_custom_nodes_versions.py [--dry-run] [--input custom_nodes.json] +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timezone +from pathlib import Path + +try: + from packaging.version import InvalidVersion, Version +except ImportError: + print("Requires: pip install packaging", file=sys.stderr) + sys.exit(1) + +HERE = Path(__file__).resolve().parent +DEFAULT_INPUT = HERE / "custom_nodes.json" + + +def _run_git(args: list[str], timeout: int = 60) -> tuple[int, str, str]: + p = subprocess.run( + ["git", *args], + capture_output=True, + text=True, + timeout=timeout, + ) + return p.returncode, p.stdout or "", p.stderr or "" + + +def _normalize_tag_for_version(tag: str) -> str: + """Return ref suitable for git checkout (strip refs/tags/ prefix).""" + if tag.startswith("refs/tags/"): + tag = tag[len("refs/tags/") :] + if tag.endswith("^{}"): + tag = tag[:-3] + return tag + + +def _parse_tags(ls_remote_out: str) -> list[str]: + tags: list[str] = [] + for line in ls_remote_out.splitlines(): + line = line.strip() + if not line or "\t" not in line: + continue + _sha, ref = line.split("\t", 1) + if not ref.startswith("refs/tags/"): + continue + if ref.endswith("^{}"): + continue + tags.append(_normalize_tag_for_version(ref)) + return tags + + +def _version_sort_key(tag: str) -> tuple: + t = tag.strip() + if t.startswith("v."): + t = "v" + t[2:] + if t.startswith("v"): + core = t[1:] + else: + core = t + core = core.removesuffix("-main") + try: + return (0, Version(core)) + except InvalidVersion: + return (1, tag) + + +def _latest_semver_tag(repo: str) -> str | None: + code, out, err = _run_git(["ls-remote", "--refs", "--tags", repo]) + if code != 0: + return None + tags = _parse_tags(out) + semver_tags = [t for t in tags if _version_sort_key(t)[0] == 0] + if not semver_tags: + return None + semver_tags.sort(key=_version_sort_key) + return semver_tags[-1] + + +def _default_branch_tip(repo: str) -> str | None: + code, out, _ = _run_git(["ls-remote", "--symref", repo, "HEAD"]) + if code != 0 or not out.strip(): + return None + # ref: refs/heads/main\tHEAD + m = re.search(r"ref:\s+refs/heads/(\S+)\s+HEAD", out) + branch = m.group(1) if m else None + if not branch: + for b in ("main", "master"): + code2, out2, _ = _run_git(["ls-remote", repo, f"refs/heads/{b}"]) + if code2 == 0 and out2.strip(): + branch = b + break + else: + return None + code3, out3, _ = _run_git(["ls-remote", repo, f"refs/heads/{branch}"]) + if code3 != 0 or not out3.strip(): + return None + sha = out3.strip().split("\t", 1)[0] + return sha[:12] if len(sha) >= 12 else sha + + +def _resolve_version(repo: str, old_version: str) -> tuple[str | None, str]: + if old_version.lower() == "latest" or old_version == "": + tip = _default_branch_tip(repo) + if tip: + return tip, "default-branch-tip" + return None, "no-default-branch" + + tag = _latest_semver_tag(repo) + if tag: + return tag, "latest-semver-tag" + + tip = _default_branch_tip(repo) + if tip: + return tip, "fallback-default-branch-tip" + return None, "unresolved" + + +def _process_node(n: dict) -> tuple[str, str, str, str | None, str]: + """Returns (id, repo, old_version, new_version_or_None, how_or_reason).""" + repo = n.get("repository") or "" + if not repo: + return (str(n.get("id", "")), "", "", None, "no-repository") + old = str(n.get("version") or "") + nid = str(n.get("id", repo)) + new_v, how = _resolve_version(repo, old) + return (nid, repo, old, new_v, how) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--input", type=Path, default=DEFAULT_INPUT) + ap.add_argument("--dry-run", action="store_true") + ap.add_argument("--jobs", type=int, default=8, help="parallel git ls-remote workers") + args = ap.parse_args() + + data = json.loads(args.input.read_text(encoding="utf-8")) + nodes = data.get("custom_nodes") or [] + failures: list[tuple[str, str, str]] = [] + + with ThreadPoolExecutor(max_workers=max(1, args.jobs)) as ex: + futs = {ex.submit(_process_node, n): n for n in nodes} + for fut in as_completed(futs): + nid, repo, old, new_v, how = fut.result() + n = futs[fut] + if not new_v: + failures.append((nid, repo, how)) + continue + if new_v == old: + continue + print(f"{nid}: {old!r} -> {new_v!r} ({how})", flush=True) + if not args.dry_run: + n["version"] = new_v + + meta = data.setdefault("metadata", {}) + meta["generated_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%d") + meta["version_resolve_note"] = ( + "versions resolved by update_custom_nodes_versions.py " + "(prefer latest semver tag, else default-branch SHA prefix)" + ) + + if args.dry_run: + print("\n--dry-run: not writing file") + if failures: + print("\nFailures:", file=sys.stderr) + for row in failures: + print(" ", row, file=sys.stderr) + return 1 if failures else 0 + + args.input.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"\nWrote {args.input}") + if failures: + print("\nUnresolved (left unchanged where applicable):", file=sys.stderr) + for row in failures: + print(" ", row, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())