AI活用

ComfyUIカスタムノードを自動テスト・公開する|CI/CD入門

ComfyUIカスタムノードのCI/CDは、純Python部分のpytest、ComfyUI環境でのImport Smoke Test、Tagと連動したReleaseの3層で組み、公開は手動承認のGateを残す形が最小です。何を自動で確かめ、何を人が判断するかを分けて解説します。

この記事の目次
  1. 結論:自動で確かめるのは3層、公開の最終判断だけ人が押す
  2. 何を自動テストするか
  3. 純Python部分をどう分離するか
  4. 単体Testはどこから始めるか
  5. Import Smoke Testをどう組むか
  6. 正常2件・異常2件+Smoke Testの記録表
  7. Version・TagとReleaseをどう結ぶか
  8. 自動公開のGateはどこに置くか
  9. よくある質問
  10. GPUのないCIランナーでテストする意味はありますか?
  11. ComfyUIのmasterが変わってSmoke Testが落ちたらどうしますか?
  12. Workflow実行Testまで自動化すべきですか?
  13. まとめ

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_TYPESRETURN_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.pytorchfolder_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 prefixNoneTypeError
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を残します。自動化するのは検査で、取り消せない公開の判断は人に残すのが、この記事の線引きです。

スポンサーリンク