diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bb6b5b4..9d10cfc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,28 +1,50 @@ name: Publish to Comfy registry + +# Publishes this node to the ComfyUI Registry using Comfy-Org/publish-node-action. +# +# Security notes: +# - Trigger is intentionally push-to-main (paths: pyproject.toml) or manual +# workflow_dispatch ONLY. Never pull_request_target: that trigger runs with +# this repo's secrets against a PR head from a fork, which would hand +# REGISTRY_ACCESS_TOKEN to any third-party PR author. Do not add it. +# - REGISTRY_ACCESS_TOKEN is passed only as the action's declared input; it is +# never echoed, printed, or exported to a shell step. GitHub Actions masks +# the literal secret value in all logs regardless. +# - GITHUB_TOKEN permissions are minimal: read-only, and only what +# actions/checkout needs to fetch the repo. The publish step itself does not +# use GITHUB_TOKEN at all — it authenticates to the Registry solely with +# REGISTRY_ACCESS_TOKEN. on: - workflow_dispatch: push: branches: - main - - master paths: - "pyproject.toml" + workflow_dispatch: {} permissions: - issues: write + contents: read + +concurrency: + group: publish-to-registry + cancel-in-progress: false jobs: publish-node: name: Publish Custom Node to registry runs-on: ubuntu-latest + # Belt-and-suspenders: even if this workflow file were ever present on a + # fork, publishing only ever runs in the canonical repo. if: ${{ github.repository_owner == 'LuqP2' }} steps: - name: Check out code uses: actions/checkout@v4 with: submodules: true + - name: Publish Custom Node uses: Comfy-Org/publish-node-action@v1 with: - ## Add your own personal access token to your Github Repository secrets and reference it here. + # Configure this once in Settings -> Secrets and variables -> Actions. + # Never place the token value directly in this file. personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} diff --git a/docs/registry-compatibility-audit.md b/docs/registry-compatibility-audit.md new file mode 100644 index 0000000..463adbd --- /dev/null +++ b/docs/registry-compatibility-audit.md @@ -0,0 +1,175 @@ +# Auditoria de compatibilidade — `pyproject.toml` / ComfyUI Registry + +Investigação do **código** (não do README, não do template comentado) para decidir +o que é seguro declarar em `classifiers`, `requires-python`, `Icon`/`Banner` e +`requires-comfyui`, conforme +[docs.comfy.org/registry/specifications](https://docs.comfy.org/registry/specifications). + +Este documento é só o relatório. O `pyproject.toml` **não foi alterado** — +a proposta está no bloco de código no final, para aplicação manual. + +--- + +## 1. Menor versão de Python que o código realmente aceita + +**Resposta: Python ≥ 3.10.** + +Evidência: [`metahub_input_node.py:69`](../metahub_input_node.py#L69) + +```python +def mask_to_tensor(mask_path: Path | None, width: int, height: int) -> torch.Tensor: +``` + +O arquivo não tem `from __future__ import annotations` (confirmado por busca em +todo o repo), então essa anotação é avaliada **em tempo de definição da função**, +não como string. `Path | None` usando o operador `|` em tipos reais é +[PEP 604](https://peps.python.org/pep-0604/), introduzido no Python 3.10 — em +3.9 ou anterior isso levanta `TypeError: unsupported operand type(s) for |` +na hora de importar o módulo, ou seja, o node inteiro falha ao carregar. + +Constraint mais fraca, mesma direção: [`metahub_input_node.py:46`](../metahub_input_node.py#L46) +usa `-> tuple[dict, str]` (PEP 585, genéricos nativos em minúsculo), que exige +apenas 3.9+ — já coberto pelo piso de 3.10 acima. + +Não há nenhum outro uso de sintaxe mais nova (sem `match/case`, sem `:=`, +sem `except*`, sem `str.removeprefix/removesuffix`, sem `tomllib`) em nenhum +outro arquivo do pacote. + +--- + +## 2. Windows / Linux / macOS — funciona igual? Existe caminho platform-dependent? + +**Resposta: sim, funciona igual nos três. Há só um ponto assimétrico, não-crítico.** + +- Todo I/O de arquivo passa por `pathlib.Path` (não há concatenação manual de + string com `\` ou `/`, não há `os.path.join` platform-fráigl misturado com + literais). Ex.: [`metahub_input_node.py:20-43`](../metahub_input_node.py#L20-L43), + [`metadata_utils_impl.py`](../metadata_utils_impl.py) inteiro. +- Busca em todo o repo por `sys.platform`, `os.name`, `platform.system`, + `ctypes`, `winreg`, `msvcrt`, `os.startfile`, chamadas POSIX-only + (`os.geteuid`, `fcntl`, `pwd`/`grp`) e `subprocess(..., shell=True)`: + **zero ocorrências** em qualquer arquivo `.py` do pacote. +- Único ponto assimétrico: [`video_metadata_utils.py:42-51`](../video_metadata_utils.py#L42-L51), + a lista de fallback de localização do `ffmpeg` (`find_ffmpeg_binary`) só tem + caminhos hard-coded do Windows (`C:/ffmpeg/bin/ffmpeg.exe` etc.) depois de já + ter tentado `FFMPEG_PATH` e `shutil.which('ffmpeg')` — que são + cross-platform e cobrem o caso comum em Linux/macOS (ffmpeg instalado via + apt/brew fica no PATH). Se não encontrar em nenhum dos dois, o código levanta + um `RuntimeError` com instrução clara ([`video_metadata_utils.py:317-322`](../video_metadata_utils.py#L317-L322)) + — não trava, não corrompe nada, só falha de forma explícita e igual nas + três plataformas quando o ffmpeg realmente não está instalado. + +Não há evidência de comportamento *diferente* ou incorreto em nenhuma +plataforma — só uma heurística de descoberta de binário um pouco mais fraca +fora do Windows. Isso sustenta o classifier `OS Independent`. + +--- + +## 3. Toca em GPU/CUDA/torch, ou é I/O + hashing puro? + +**Resposta: usa `torch`, mas nunca como dependência de GPU/CUDA. Nenhuma chamada +de device placement em lugar nenhum do repo (busca por `.cuda(`, `.to(device`, +`.half()`, `torch.device` — zero ocorrências).** + +Dois usos, ambos read-only / opcionais: + +1. **Telemetria de GPU, defensiva** — [`metadata_utils_impl.py:196-256`](../metadata_utils_impl.py#L196-L256) + `collect_gpu_metrics()`: faz `import torch` dentro de um `try/except + ImportError`, depois só *lê* `torch.cuda.is_available()`, + `torch.cuda.get_device_name(0)`, `torch.cuda.max_memory_allocated(0)`, e + equivalente para `torch.backends.mps`. Se CUDA/MPS não existir, cai para + `"CPU (CUDA not available)"` sem erro. Nunca aloca nada na GPU, nunca move + um tensor para lá — só introspecciona o que já está rodando. +2. **Criação de tensor CPU para o contrato de tipos do ComfyUI** — + [`metahub_input_node.py:61-76`](../metahub_input_node.py#L61-L76) + (`image_to_tensor`, `mask_to_tensor`): usa `torch.from_numpy(array)[None,]` + para produzir os tipos `IMAGE`/`MASK` que o ComfyUI espera. Isso roda em + CPU; não há `.to()`/`.cuda()` em seguida. + +Fora isso, o node é I/O (leitura/escrita de PNG/JPEG/WebP/GLB, subprocess para +ffmpeg) + hashing (SHA256 de modelos/LoRAs) + string/JSON building. Ele +funciona sem problema em instalações ComfyUI CPU-only. + +**Conclusão prática:** declarar `Environment :: GPU :: NVIDIA CUDA` (ou +ROCm/Metal) seria uma claim falsa — o node não exige GPU nenhuma para +funcionar, e isso só polui o filtro de busca de quem procura nodes que +realmente *precisam* de CUDA. Nenhum classifier de acelerador deve ser +adicionado. + +--- + +## 4. Existe API do ComfyUI para estabelecer uma versão mínima verificável? + +**Resposta: não, o código não usa nenhuma.** + +Os dois únicos pontos de contato com o runtime do ComfyUI além dos hidden +inputs padrão (`PROMPT`, `EXTRA_PNGINFO`, `UNIQUE_ID` — parte estável do +contrato de custom nodes desde as primeiras versões) são: + +- `import folder_paths` — [`metadata_utils_impl.py:59`](../metadata_utils_impl.py#L59) + e [`metadata_utils_impl.py:1091`](../metadata_utils_impl.py#L1091), ambos + dentro de `try/except`, usados só para localizar pastas de modelos / diretório + de output. Se falhar, cai para caminhos relativos default. +- `import comfy` / `comfy.__version__` — [`metadata_utils_impl.py:297-299`](../metadata_utils_impl.py#L297-L299), + também dentro de `try/except`, usado só para *reportar* a versão do ComfyUI + nos metadados salvos — nunca comparado contra um piso mínimo. + +Nenhum desses é usado como *gate* (não há `if comfy_version < X: raise` nem +equivalente), e nenhuma API nova/versionada é chamada — nada que dependa de +uma versão específica do ComfyUI para existir. + +**Conclusão prática:** não há piso de versão do ComfyUI provável a partir do +código. Preencher `requires-comfyui` seria um chute. Se o mantenedor já +validou manualmente uma versão mínima em teste real, isso é um fato externo +ao código-fonte (não deriva de auditoria) e pode ser adicionado depois, com +essa evidência explícita. + +--- + +## Princípio aplicado + +Cada campo abaixo só foi preenchido onde havia evidência de código +(arquivo:linha) sustentando um limite real. Onde não havia prova, o campo foi +**omitido**, não preenchido "por via das dúvidas". + +## `pyproject.toml` proposto (não aplicado — para revisão) + +```toml +[project] +name = "imagemetahub-comfyui-save" +description = "Official companion node for Image MetaHub. Saves A1111/Civitai-compatible parameters plus extended Image MetaHub metadata (workflow JSON, SHA256 model hashes, VRAM peak/total, GPU device, generation time, and steps/sec) for reproducibility and benchmarking." +version = "1.1.9" +license = {file = "LICENSE"} +requires-python = ">=3.10" +classifiers = [ + "Operating System :: OS Independent", +] + +dependencies = [] + +[project.urls] +Repository = "https://github.com/LuqP2/ImageMetaHub-ComfyUI-Save" +# Used by Comfy Registry https://registry.comfy.org +Documentation = "https://github.com/LuqP2/ImageMetaHub-ComfyUI-Save/wiki" +"Bug Tracker" = "https://github.com/LuqP2/ImageMetaHub-ComfyUI-Save/issues" + +[tool.comfy] +PublisherId = "image-metahub" +DisplayName = "ImageMetaHub-ComfyUI-Save" +includes = [] +# Icon and Banner omitted: no square icon (<=400x400) or 21:9 banner asset +# exists yet. Add them (with real URLs) when they do — an empty string is +# not the same as omitting the field. +# "requires-comfyui" omitted: no code path checks or requires a specific +# ComfyUI version (see docs/registry-compatibility-audit.md, Q4). Add it only +# once a floor version has actually been tested, not as a guess. +``` + +**Diffs em relação ao atual, e por quê:** + +| Campo | Atual | Proposto | Motivo | +|---|---|---|---| +| `requires-python` | ausente | `">=3.10"` | `metahub_input_node.py:69` usa `Path \| None` sem `from __future__ import annotations` | +| `classifiers` | bloco inteiro comentado | só `OS Independent` | nenhum caminho platform-gated encontrado; nenhum classifier de GPU porque o node não exige GPU | +| `Icon` | `""` | omitido | string vazia ≠ campo omitido; não existe asset ainda | +| `requires-comfyui` | comentado | continua omitido | nenhuma API de versão mínima é usada no código |