chore(ci): harden Registry publish workflow permissions + compatibility audit

Publish workflow (.github/workflows/publish.yml) already existed and was
already push-to-main-only / workflow_dispatch with no pull_request_target.
Hardened further:

- permissions: issues: write -> contents: read (the action never uses
  GITHUB_TOKEN at all; checkout only needs read access, nothing else)
- drop the unused 'master' branch trigger (repo only has main)
- add a concurrency group so overlapping publish runs can't race
- document, inline, why pull_request_target must never be added here and
  how the REGISTRY_ACCESS_TOKEN secret is handled (never echoed/logged,
  passed only as the action's declared input)

Also adds docs/registry-compatibility-audit.md: a code-only audit (no
README, no template comments) answering the four compatibility questions
for pyproject.toml (min Python version, OS support, GPU/CUDA usage, ComfyUI
min-version API), each with file:line evidence, plus a proposed
pyproject.toml block for manual review. pyproject.toml itself is NOT
touched by this PR.

No secret is configured or requested by this change, and nothing is
published. Node runtime behavior is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
LuqP2
2026-08-07 20:39:25 -03:00
co-authored by Claude Sonnet 5
parent 814b79a011
commit e9d210ee45
2 changed files with 201 additions and 4 deletions
+26 -4
View File
@@ -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 }}
+175
View File
@@ -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 |