diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 24c58159..c61fcdeb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v7 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/ci-cursor-review.yml b/.github/workflows/ci-cursor-review.yml deleted file mode 100644 index 0ca5faca..00000000 --- a/.github/workflows/ci-cursor-review.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Description: Team-gated multi-model Cursor review — a thin caller for the -# reusable workflow in Comfy-Org/github-workflows, which is the single source of -# truth for the panel, judge, prompts, and scripts. Triggered by the -# 'cursor-review' label. -# -# Access control (team-only, two layers): -# 1. Only users with triage permission or higher can apply a label in a public -# repo, so the public cannot trigger this. -# 2. The reusable workflow's secret-bearing jobs do not run on fork PRs (forks -# get no secrets), so CURSOR_API_KEY is reachable only on internal branches. -name: CI - Cursor Review - -on: - pull_request: - types: [labeled, unlabeled] - -permissions: - contents: read - pull-requests: write - -concurrency: - # Re-labeling cancels an in-flight run for the same PR + label. - group: cursor-review-pr-${{ github.event.pull_request.number }}-${{ github.event.label.name }} - cancel-in-progress: true - -jobs: - cursor-review: - if: github.event.action == 'labeled' && github.event.label.name == 'cursor-review' - # SHA-pinned. Bump this SHA to pick up upstream changes; keep - # `workflows_ref` matching so prompts/scripts load from the same commit as - # the workflow definition. - uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@047ca48febe3a6647608ed2e0c4331b491cb9d6a # github-workflows#9 - with: - # Overriding diff_excludes replaces the reusable default wholesale, so - # this restates the generated/vendored defaults and adds this repo's - # heavy paths (uv lockfile, demo media, bundled uv test fixtures). - diff_excludes: >- - :!**/package-lock.json - :!**/yarn.lock - :!**/pnpm-lock.yaml - :!**/node_modules/** - :!**/.claude/** - :!**/dist/** - :!**/vendor/** - :!**/*.generated.* - :!**/*.min.js - :!**/*.min.css - :!uv.lock - :!assets/** - :!tests/uv/** - # Load the prompts/scripts from the same ref as `uses:`. - workflows_ref: 047ca48febe3a6647608ed2e0c4331b491cb9d6a - secrets: - CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} - # Optional — enables start/complete Slack DMs to the triggerer. - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index b75397e5..330c1e18 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -8,7 +8,7 @@ on: permissions: actions: write - contents: read # 'read' is enough because signatures live in a REMOTE repo + contents: read # 'read' is enough because signatures live in a REMOTE repo pull-requests: write statuses: write @@ -16,42 +16,14 @@ jobs: cla-assistant: runs-on: ubuntu-latest steps: - # The CLA action normally requires every commit author in a PR to sign. - # We only want the PR author to sign, so we allowlist all other committers - # by computing them from the PR's commits and excluding the PR author. - - name: Build author-only allowlist - id: allowlist - if: > - github.event_name == 'pull_request_target' || - (github.event_name == 'issue_comment' && github.event.issue.pull_request && ( - github.event.comment.body == 'recheck' || - github.event.comment.body == 'I have read and agree to the Contributor License Agreement' - )) - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - PR_AUTHOR: ${{ github.event.pull_request.user.login || github.event.issue.user.login }} - BASE_ALLOWLIST: action@github.com,actions-user,ampagent,claude,comfy-pr-bot,GitHub Action,github-actions,github-actions[bot],Glary Bot,Glary-Bot,*[bot] - run: | - others=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits" --paginate \ - --jq '.[] | (.author.login // empty), (.committer.login // empty)' \ - | sort -u | grep -vix "${PR_AUTHOR}" | paste -sd, -) - if [ -n "$others" ]; then - echo "allowlist=${BASE_ALLOWLIST},${others}" >> "$GITHUB_OUTPUT" - else - echo "allowlist=${BASE_ALLOWLIST}" >> "$GITHUB_OUTPUT" - fi - - name: CLA Assistant # Run on PR events, on "recheck" comment, or when someone posts the exact signing phrase. # IMPORTANT: this phrase must match `custom-pr-sign-comment` below. if: > github.event_name == 'pull_request_target' || - (github.event_name == 'issue_comment' && github.event.issue.pull_request && ( - github.event.comment.body == 'recheck' || - github.event.comment.body == 'I have read and agree to the Contributor License Agreement' - )) - uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 + github.event.comment.body == 'recheck' || + github.event.comment.body == 'I have read and agree to the Contributor License Agreement' + uses: contributor-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # PAT required to write to the centralized signatures repo. @@ -66,10 +38,9 @@ jobs: path-to-signatures: signatures/cla.json branch: main - # Only the PR author must sign: bots plus every non-author committer - # are allowlisted via the "Build author-only allowlist" step above. + # Allowlist bots so they don't need to sign (optional, comma-separated). # *[bot] is a catch-all for any GitHub App bot account. - allowlist: ${{ steps.allowlist.outputs.allowlist }} + allowlist: ampagent,claude,coderabbitai[bot],comfy-pr-bot,dependabot[bot],github-actions[bot],copilot-swe-agent[bot],devin-ai-integration[bot],*[bot] # Custom PR comment messages custom-notsigned-prcomment: | diff --git a/.github/workflows/publish_package.yml b/.github/workflows/publish_package.yml index edfdd961..ef3942fa 100644 --- a/.github/workflows/publish_package.yml +++ b/.github/workflows/publish_package.yml @@ -14,7 +14,7 @@ jobs: permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c0d45cca..65939d40 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/ruff_check.yml b/.github/workflows/ruff_check.yml index 8358e1f6..fa9b5673 100644 --- a/.github/workflows/ruff_check.yml +++ b/.github/workflows/ruff_check.yml @@ -15,7 +15,7 @@ jobs: ruff_check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/run-on-gpu.yml b/.github/workflows/run-on-gpu.yml index 9d98fdaa..ee916cd8 100644 --- a/.github/workflows/run-on-gpu.yml +++ b/.github/workflows/run-on-gpu.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v7 + uses: actions/checkout@v6 - name: Check Nvidia run: | diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index eb619f56..cb7804c3 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v7 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index febabaec..a2c2f21b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v7 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index fd09aba0..7fa60719 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -912,6 +912,14 @@ def validate( except Exception: pass + # env var fallback (same resolution as `comfy run`) + from comfy_cli.env_backend import get_backend_from_env + env_h, env_p = get_backend_from_env() + if not host and env_h: + host = env_h + if not port and env_p is not None: + port = env_p + try: graph = Graph.load(mode=mode, input_path=input_path, host=host or "127.0.0.1", port=port or 8188) except LoadError as e: diff --git a/comfy_cli/env_backend.py b/comfy_cli/env_backend.py new file mode 100644 index 00000000..a1a356fa --- /dev/null +++ b/comfy_cli/env_backend.py @@ -0,0 +1,102 @@ +"""Shared environment-variable backend resolver for ComfyUI host/port. + +Supports four env vars (highest to lowest precedence within the env layer): + COMFY_BACKEND=host:port (combined, colon-separated) + COMFY_HOST_PORT=host:port (combined, same as COMFY_BACKEND) + COMFY_HOST + COMFY_PORT (separate) + +CLI flags always take precedence over env vars; this helper is a fallback. +""" +from __future__ import annotations + +import os +import re +from typing import Optional + +# IPv6 address detection: contains multiple colons or starts with [ +_IPV6_RE = re.compile(r"^\[.+\]$|^([0-9a-fA-F]{0,4}:){2,}") + + +def get_backend_from_env() -> tuple[Optional[str], Optional[int]]: + """Resolve host/port from environment variables. + + Returns (host, port) where either may be None if not set. + Handles IPv6 bracketed hosts (e.g. ``[::1]:8188``). + Logs a warning on invalid port values instead of silently swallowing. + """ + be = os.environ.get("COMFY_BACKEND") or os.environ.get("COMFY_HOST_PORT") + if be: + b = be.replace("http://", "").replace("https://", "").strip("/") + host, port = _split_host_port(b) + if port is not None and port is not None and not _valid_port(port): + import logging + logging.getLogger(__name__).warning( + "Invalid port %r in COMFY_BACKEND/COMFY_HOST_PORT, ignoring", port + ) + return host, None + return host, port + + h = os.environ.get("COMFY_HOST") + p = os.environ.get("COMFY_PORT") + if h or p: + port: Optional[int] = None + if p: + try: + port = int(p) + except ValueError: + import logging + logging.getLogger(__name__).warning( + "Invalid port %r in COMFY_PORT, ignoring", p + ) + return h, None + if not _valid_port(port): + logging.getLogger(__name__).warning( + "Port %d out of range in COMFY_PORT, ignoring", port + ) + return h, None + return h, port + + return None, None + + +def _split_host_port(s: str) -> tuple[Optional[str], Optional[int]]: + """Split a host:port string, handling IPv6 bracketed hosts. + + Examples: + "127.0.0.1:8188" -> ("127.0.0.1", 8188) + "[::1]:8188" -> ("[::1]", 8188) + "[::1]" -> ("[::1]", None) + "localhost:8188" -> ("localhost", 8188) + "::1" -> ("::1", None) (bare IPv6, no port) + """ + # Bracketed IPv6: [host]:port or [host] + if s.startswith("["): + end = s.find("]") + if end == -1: + return s, None # malformed, return as-is + host = s[:end + 1] # keep brackets + rest = s[end + 1:] + if rest.startswith(":"): + try: + return host, int(rest[1:]) + except ValueError: + return host, None + return host, None + + # Bare IPv6 (multiple colons, no brackets) — no port + if s.count(":") > 1: + return s, None + + # Normal host:port + if ":" in s: + h, p = s.rsplit(":", 1) + try: + return h, int(p) + except ValueError: + return h, None + return s, None + + +def _valid_port(port: int) -> bool: + """Check if a port number is in valid range.""" + return 1 <= port <= 65535 diff --git a/comfy_cli/host_port.py b/comfy_cli/host_port.py index bab4f6b4..01ffdf81 100644 --- a/comfy_cli/host_port.py +++ b/comfy_cli/host_port.py @@ -81,9 +81,26 @@ def _to_port(s: str, original: str) -> int: def resolve_host_port(host: str | None, port: int | None) -> tuple[str, int]: - """Fill host/port from ``config.background`` then defaults; validate and - bracket IPv6 literals so callers building ``'http://{host}:{port}'`` get a - well-formed URL (e.g. ``'::1'`` -> ``'[::1]'``).""" + """Fill host/port from environment variables, then ``config.background``, + then defaults; validate and bracket IPv6 literals so callers building + ``'http://{host}:{port}'`` get a well-formed URL (e.g. ``'::1'`` -> + ``'[::1]'``). + + Resolution order (highest to lowest): + 1. Explicit ``host`` / ``port`` arguments (from CLI flags). + 2. Environment variables: ``COMFY_BACKEND``, ``COMFY_HOST_PORT``, + ``COMFY_HOST`` / ``COMFY_PORT``. + 3. Persisted ``config.background`` server. + 4. Built-in defaults (``127.0.0.1:8188``). + """ + from comfy_cli.env_backend import get_backend_from_env + + env_h, env_p = get_backend_from_env() + if not host and env_h: + host = env_h + if not port and env_p is not None: + port = env_p + cfg = ConfigManager() bg = cfg.background if not host and bg is not None: