ComfyUIカスタムノードのCI/CDは、「純Python部分のpytest」「ComfyUI環境でのImport Smoke Test」「Tagと連動したRelease」の3層で組み、Registryへの公開だけは手動承認のGateを残す形が最小です。Importが通ることと、nodeが正しく動くことは別の検査なので、両方を自動化の対象にします。
情報確認日:2026年8月22日(日本時間)
結論:自動で確かめるのは3層、公開の最終判断だけ人が押す
責任範囲
- 処理本体をComfyUIに依存しない純Python関数に分け、pytestで正常・異常入力を検査する
- ComfyUI本体を取得した環境でnodeをimportし、
NODE_CLASS_MAPPINGSが読めることをSmoke Testにする pyproject.tomlのversionとGit tagを一致させ、tagをReleaseの唯一の起点にする- Registry公開は
workflow_dispatchか承認付き環境で、main pushだけでは走らせない - nodeの作り方はカスタムノードの作り方、publisher登録や審査はComfy Registryへの公開に任せる
何を自動テストするか
自動化の対象は「Importできるか」だけでは足りません。ComfyUIは起動時にcustom_nodesを走査し、エラーがあっても本体は起動を続けて失敗を報告する仕組みです。つまりImportの成否はログを見れば分かりますが、nodeの出力が正しいか、異常入力で期待どおり例外を出すかは、別に検査しない限り誰も確かめません。
| 検査 | 何を確かめるか | ComfyUI本体 | GPU |
|---|---|---|---|
| 単体Test(pytest) | 処理関数の正常出力と、異常入力での例外 | 不要 | 不要 |
| Import Smoke Test | ComfyUI環境でnodeモジュールが読み込め、mappingsが空でない | 必要(clone) | 不要 |
| Workflow実行Test | 実際のworkflowを通して出力が出る | 必要 | 多くの場合必要 |
この記事では上の2つを扱います。3つ目のWorkflow実行Testは、公式がGitHub Actions上でworkflow.jsonを実行する「Comfy-Action」を案内していますが、具体的な設定は公式ドキュメントで最新の案内を確認してください。最初の2層が安定してから足す段階です。
純Python部分をどう分離するか
テストしやすさは、nodeクラスと処理関数を分けた時点でほぼ決まります。nodeクラスはINPUT_TYPESやRETURN_TYPESといったComfyUIの約束を担当し、処理そのものは別ファイルの関数に置きます。
my_text_tools/
├── __init__.py # NODE_CLASS_MAPPINGS を export
├── nodes.py # node クラス(ComfyUI の約束だけ)
├── core.py # 純 Python の処理関数(ComfyUI 非依存)
├── tests/
│ ├── test_core.py # pytest
│ └── test_import.py # Import Smoke Test
├── pyproject.toml
└── requirements.txt
# core.py
def prefix_text(text: str, prefix: str) -> str:
if not isinstance(text, str):
raise TypeError(f"text must be str, got {type(text).__name__}")
if not isinstance(prefix, str):
raise TypeError(f"prefix must be str, got {type(prefix).__name__}")
return prefix + text.strip()
# nodes.py
from .core import prefix_text
class PrefixText:
CATEGORY = "my_tools/text"
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"text": ("STRING", {"multiline": True, "default": ""}),
"prefix": ("STRING", {"default": "[prefix] "}),
}}
def run(self, text, prefix):
return (prefix_text(text, prefix),)
core.pyはtorchもfolder_pathsもimportしません。この状態なら、ComfyUIを入れていないCIランナーでもpytestが数秒で走ります。画像tensorを扱うnodeでも、「tensorを受け取って計算する関数」と「ComfyUIから値を受け取るnode」は同じように分けられます。
単体Testはどこから始めるか
pytestで正常2件・異常2件から始めます。件数が少ないのは意図的で、CIが動く形を先に作り、nodeの機能が増えるたびに足していきます。
# tests/test_core.py
import pytest
from my_text_tools.core import prefix_text
def test_prefix_and_strip():
assert prefix_text(" hello ", "[p] ") == "[p] hello"
def test_empty_text_returns_prefix_only():
assert prefix_text("", "[p] ") == "[p] "
def test_non_string_text_raises():
with pytest.raises(TypeError):
prefix_text(123, "[p] ")
def test_non_string_prefix_raises():
with pytest.raises(TypeError):
prefix_text("hello", None)
異常系で確かめたいのは「壊れた入力で黙って変な値を返さない」ことです。pytest.raisesで例外の型まで固定しておくと、あとで例外の種類を変えたときにテストが教えてくれます。
# .github/workflows/test.yml
name: test
on:
push:
pull_request:
jobs:
unit:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install pytest -r requirements.txt
- run: pytest tests/test_core.py -q
Actionのversion(@v4など)は執筆時点の一般的な指定で、GitHub側の最新を確認して置き換えてください。
Import Smoke Testをどう組むか
Import Smoke Testは「ComfyUI本体があるPython環境で、自分のnodeをimportしたらNODE_CLASS_MAPPINGSが読めるか」だけを見ます。GPUは使わず、ComfyUIも起動しません。ComfyUIをcloneしてsys.pathに通すことで、folder_pathsなど本体モジュールへのimportが解決できる状態を作ります。
# tests/test_import.py
import importlib
import os
import sys
COMFY_ROOT = os.environ.get("COMFY_ROOT", "")
def test_node_mappings_importable():
assert COMFY_ROOT, "COMFY_ROOT が未設定"
sys.path.insert(0, COMFY_ROOT)
mod = importlib.import_module("my_text_tools")
assert hasattr(mod, "NODE_CLASS_MAPPINGS")
assert "PrefixText" in mod.NODE_CLASS_MAPPINGS
node_cls = mod.NODE_CLASS_MAPPINGS["PrefixText"]
assert "required" in node_cls.INPUT_TYPES()
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: my_text_tools
- uses: actions/checkout@v4
with:
repository: comfyanonymous/ComfyUI
ref: master
path: ComfyUI
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install torch --index-url https://download.pytorch.org/whl/cpu
- run: pip install -r ComfyUI/requirements.txt -r my_text_tools/requirements.txt pytest
- run: pytest my_text_tools/tests/test_import.py -q
env:
COMFY_ROOT: ${{ github.workspace }}/ComfyUI
ref: masterは「常に最新のComfyUIで壊れないか」を見る設定です。特定のtagに固定すれば「対応を宣言したversionで通るか」の検査になります。どちらを取るかは目的次第で、両方並べるとmatrixが増えます。
Version Matrixは増やしすぎないでください。Python 2種類×ComfyUI 1〜2種類で、最初は十分です。組み合わせを増やすほど、失敗したときに「どの軸が原因か」を読む時間が増えます。
正常2件・異常2件+Smoke Testの記録表
CIを初めて通したときの結果は、後から「いつから壊れたか」を遡る基準になります。数値ではなく、どのjobがどのcommitで通ったかを残します。
| 検査 | 対象 | commit | 結果 | 失敗時のログ要点 |
|---|---|---|---|---|
| 正常1 | 前後空白の除去と接頭辞付与 | |||
| 正常2 | 空文字で接頭辞のみ返る | |||
| 異常1 | textが非文字列でTypeError |
|||
| 異常2 | prefixがNoneでTypeError |
|||
| Smoke | ComfyUI master環境でNODE_CLASS_MAPPINGSが読める |
Version・TagとReleaseをどう結ぶか
versionの正本はpyproject.tomlの[project] version一つにします。Registryもsemantic versioningを求めており、ここが唯一の真実であれば、Git tagはその写しとして扱えます。
| 規約 | 内容 | 破ると起きること |
|---|---|---|
| version=tag | version = "0.2.0"を上げたcommitにv0.2.0を打つ |
Registryの版とGitの版が食い違う |
| tag=Release | tagを打ったときだけReleaseを作る。手動でReleaseを作らない | Releaseに対応するcommitが不明になる |
| 1 tag = 1 commit | tagを打ち直さない。直すなら次のpatch版 | 同じ版で中身が違うものが配られる |
| CHANGELOG | 破壊的変更・新機能・修正を版ごとに1行ずつ | 利用者がいつ何が変わったか追えない |
# version を上げて commit してから、同じ値で tag を打つ
git commit -am "release: 0.2.0"
git tag v0.2.0
git push origin main --tags
CI側で「tagの値とpyproject.tomlのversionが一致するか」を検査しておくと、打ち間違いを公開前に止められます。数行のPythonで十分です。
自動公開のGateはどこに置くか
公式ドキュメントが案内するGitHub Actionは、pyproject.tomlの変更がmainにpushされるとComfy-Org/publish-node-actionがRegistryへ公開する構成です。便利ですが、この形だとversionを上げたcommitを押した瞬間に公開されます。テストが落ちていても関係ありません。
この記事では、公式の形を土台に2つのGateを足します。テストjobの成功を前提条件にすること、そして人が公開ボタンを押すことです。
# .github/workflows/publish_action.yml
name: Publish to Comfy registry
on:
workflow_dispatch: # 人が手動で起動する
jobs:
test:
uses: ./.github/workflows/test.yml
publish-node:
name: Publish Custom Node to registry
needs: test # テストが通らなければ走らない
runs-on: ubuntu-latest
environment: registry # GitHub Environments で承認者を設定しておく
steps:
- name: Check out code
uses: actions/checkout@v7
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
公式yamlとの差分は3か所です。on:からpushを外してworkflow_dispatchだけにした点、needs: testでテストを前提にした点、environmentで承認者を挟める点です。GitHub Environmentsの承認機能が使えるプラン条件はGitHubの公式情報で確認してください。使えない場合でも、workflow_dispatchだけで「人が押す」Gateは成立します。
REGISTRY_ACCESS_TOKENはRegistryの公開権限を持つ鍵です。fork先からのpull requestで参照できない設定にし、ログに出力しないでください。鍵の発行と失効の手順はComfy Registryへの公開で扱います。
「main pushで即公開」を選ばない理由は、Registryに出した版は利用者の環境に届いてしまうからです。取り消せない操作の直前には、人の確認を一段置きます。
よくある質問
GPUのないCIランナーでテストする意味はありますか?
あります。純Python部分の単体TestとImport Smoke TestはGPUを使いません。nodeの壊れ方の多くはimport失敗と入力処理の誤りで、どちらもCPUで検出できます。
ComfyUIのmasterが変わってSmoke Testが落ちたらどうしますか?
自分のコード変更なしに落ちたなら、ComfyUI側の変更に追従が必要な合図です。対応したらrequires-comfyuiなどの対応version記載も更新し、patch版を出します。
Workflow実行Testまで自動化すべきですか?
nodeが画像やmodelを扱い、出力の見た目が重要になってからで足ります。公式のComfy-ActionはGitHub Actions上でworkflowを実行する仕組みですが、modelの取得やGPU runnerの用意が必要になるため、最初の2層が安定してから検討してください。
まとめ
ComfyUIカスタムノードのCI/CDは、処理を純Pythonに分けてpytestで正常・異常を検査し、ComfyUI環境でのImport Smoke Testで登録できることを確かめ、pyproject.tomlのversionとtagを一致させてReleaseを作る3層が土台です。
公開は公式のpublish-node-actionを使いつつ、workflow_dispatchとテスト前提のGateを残します。自動化するのは検査で、取り消せない公開の判断は人に残すのが、この記事の線引きです。