Instructions to use saik0s/comfy_backup with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use saik0s/comfy_backup with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="saik0s/comfy_backup", filename="ComfyUI/models/text_encoders/gemma-3-12b-it-q2_k.gguf", )
llm.create_chat_completion( messages = "No input example has been defined for this model task." )
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use saik0s/comfy_backup with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf saik0s/comfy_backup:Q4_K_S # Run inference directly in the terminal: llama cli -hf saik0s/comfy_backup:Q4_K_S
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf saik0s/comfy_backup:Q4_K_S # Run inference directly in the terminal: llama cli -hf saik0s/comfy_backup:Q4_K_S
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf saik0s/comfy_backup:Q4_K_S # Run inference directly in the terminal: ./llama-cli -hf saik0s/comfy_backup:Q4_K_S
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf saik0s/comfy_backup:Q4_K_S # Run inference directly in the terminal: ./build/bin/llama-cli -hf saik0s/comfy_backup:Q4_K_S
Use Docker
docker model run hf.co/saik0s/comfy_backup:Q4_K_S
- LM Studio
- Jan
- Ollama
How to use saik0s/comfy_backup with Ollama:
ollama run hf.co/saik0s/comfy_backup:Q4_K_S
- Unsloth Studio
How to use saik0s/comfy_backup with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for saik0s/comfy_backup to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for saik0s/comfy_backup to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for saik0s/comfy_backup to start chatting
- Pi
How to use saik0s/comfy_backup with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q4_K_S
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "saik0s/comfy_backup:Q4_K_S" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use saik0s/comfy_backup with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q4_K_S
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default saik0s/comfy_backup:Q4_K_S
Run Hermes
hermes
- Atomic Chat new
- OpenClaw new
How to use saik0s/comfy_backup with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q4_K_S
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "saik0s/comfy_backup:Q4_K_S" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use saik0s/comfy_backup with Docker Model Runner:
docker model run hf.co/saik0s/comfy_backup:Q4_K_S
- Lemonade
How to use saik0s/comfy_backup with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull saik0s/comfy_backup:Q4_K_S
Run and chat with the model
lemonade run user.comfy_backup-Q4_K_S
List all available models
lemonade list
| name: Backport Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| commit: | |
| description: 'Full 40-char SHA of the tip commit of the backport source branch (the PR head commit that passed tests). The branch is resolved from this SHA and must be unique.' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| checks: read | |
| jobs: | |
| backport-release: | |
| name: Create backport release | |
| runs-on: ubuntu-latest | |
| environment: backport release | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 | |
| with: | |
| app-id: ${{ secrets.FEN_RELEASE_APP_ID }} | |
| private-key: ${{ secrets.FEN_RELEASE_PRIVATE_KEY }} | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Configure git | |
| run: | | |
| git config user.name "fen-release[bot]" | |
| git config user.email "fen-release[bot]@users.noreply.github.com" | |
| - name: Resolve source branch from commit SHA | |
| id: resolve | |
| env: | |
| SOURCE_COMMIT: ${{ inputs.commit }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| # Require a full 40-char lowercase-hex SHA. Short SHAs are ambiguous | |
| # and we will be comparing this value against API responses (PR head | |
| # SHA, ref tips) that always return the full form. | |
| if [[ ! "${SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "::error::Input commit '${SOURCE_COMMIT}' is not a full 40-char lowercase hex SHA." | |
| exit 1 | |
| fi | |
| # Fetch all remote branches so we can search for which one(s) point | |
| # at this SHA. `actions/checkout` with fetch-depth: 0 fetches full | |
| # history of the checked-out ref but does not necessarily populate | |
| # every refs/remotes/origin/*, so do it explicitly. | |
| git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' | |
| # Verify the commit actually exists in this repo's object DB. | |
| if ! git cat-file -e "${SOURCE_COMMIT}^{commit}" 2>/dev/null; then | |
| echo "::error::Commit ${SOURCE_COMMIT} was not found in the repository." | |
| exit 1 | |
| fi | |
| # Find every remote branch whose tip == SOURCE_COMMIT. Exactly one | |
| # branch must point at it. If zero, the commit isn't anyone's tip | |
| # (likely stale, force-pushed past, or never the PR head). If more | |
| # than one, the (branch -> SHA) mapping is ambiguous and we refuse | |
| # to guess — the operator must give us a unique branch to release. | |
| mapfile -t matching_branches < <( | |
| git for-each-ref \ | |
| --format='%(refname:strip=3)' \ | |
| --points-at="${SOURCE_COMMIT}" \ | |
| refs/remotes/origin/ \ | |
| | grep -vx 'HEAD' || true | |
| ) | |
| if [[ "${#matching_branches[@]}" -eq 0 ]]; then | |
| echo "::error::No branch on origin has ${SOURCE_COMMIT} as its tip." | |
| echo "::error::Either the branch was updated after you copied this SHA, or this commit was never the head of a branch." | |
| exit 1 | |
| fi | |
| if [[ "${#matching_branches[@]}" -gt 1 ]]; then | |
| echo "::error::More than one branch on origin has ${SOURCE_COMMIT} as its tip; cannot pick one:" | |
| for b in "${matching_branches[@]}"; do | |
| echo "::error:: - ${b}" | |
| done | |
| echo "::error::Refusing to proceed with an ambiguous source branch." | |
| exit 1 | |
| fi | |
| source_branch="${matching_branches[0]}" | |
| if [[ "${source_branch}" == "${DEFAULT_BRANCH}" ]]; then | |
| echo "::error::Source branch must not be the default branch ('${DEFAULT_BRANCH}')." | |
| exit 1 | |
| fi | |
| echo "Resolved commit ${SOURCE_COMMIT} to branch '${source_branch}'." | |
| echo "source_branch=${source_branch}" >> "$GITHUB_OUTPUT" | |
| - name: Determine latest stable release | |
| id: latest | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| # List all tags matching vMAJOR.MINOR.PATCH and pick the highest by numeric | |
| # comparison of each component. We DO NOT use `sort -V` because it treats | |
| # v0.19.99 as higher than v0.20.1. | |
| latest_tag="$( | |
| git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \ | |
| | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | |
| | awk -F'[v.]' '{ printf "%010d %010d %010d %s\n", $2, $3, $4, $0 }' \ | |
| | sort -k1,1n -k2,2n -k3,3n \ | |
| | tail -n1 \ | |
| | awk '{print $4}' | |
| )" | |
| if [[ -z "${latest_tag}" ]]; then | |
| echo "::error::No stable release tags (vMAJOR.MINOR.PATCH) were found." | |
| exit 1 | |
| fi | |
| # Parse components | |
| ver="${latest_tag#v}" | |
| major="${ver%%.*}" | |
| rest="${ver#*.}" | |
| minor="${rest%%.*}" | |
| patch="${rest#*.}" | |
| new_patch=$((patch + 1)) | |
| new_version="v${major}.${minor}.${new_patch}" | |
| release_branch="release/v${major}.${minor}" | |
| latest_sha="$(git rev-list -n 1 "refs/tags/${latest_tag}")" | |
| echo "latest_tag=${latest_tag}" >> "$GITHUB_OUTPUT" | |
| echo "latest_sha=${latest_sha}" >> "$GITHUB_OUTPUT" | |
| echo "major=${major}" >> "$GITHUB_OUTPUT" | |
| echo "minor=${minor}" >> "$GITHUB_OUTPUT" | |
| echo "patch=${patch}" >> "$GITHUB_OUTPUT" | |
| echo "new_version=${new_version}" >> "$GITHUB_OUTPUT" | |
| echo "new_version_no_v=${major}.${minor}.${new_patch}" >> "$GITHUB_OUTPUT" | |
| echo "release_branch=${release_branch}" >> "$GITHUB_OUTPUT" | |
| echo "Latest stable release: ${latest_tag} (${latest_sha})" | |
| echo "New version will be: ${new_version}" | |
| echo "Release branch: ${release_branch}" | |
| - name: Validate source branch is cut directly from the latest stable release | |
| env: | |
| SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} | |
| SOURCE_COMMIT: ${{ inputs.commit }} | |
| LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }} | |
| LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} | |
| run: | | |
| set -euo pipefail | |
| # Use the user-provided SHA directly rather than re-resolving the branch | |
| # tip — the resolve step already proved the branch tip equals SOURCE_COMMIT, | |
| # and pinning to the SHA here makes the rest of the job TOCTOU-safe against | |
| # someone pushing to the branch mid-run. | |
| source_sha="${SOURCE_COMMIT}" | |
| # Walking first-parent from the source tip must reach LATEST_TAG_SHA. | |
| # We capture rev-list into a variable and grep against a here-string | |
| # rather than piping `rev-list | grep -q`: under `set -o pipefail`, | |
| # `grep -q` would exit on first match and SIGPIPE the still-streaming | |
| # `rev-list`, propagating exit 141 as a spurious "not found". | |
| first_parent_chain="$(git rev-list --first-parent "${source_sha}")" | |
| if ! grep -Fxq "${LATEST_TAG_SHA}" <<< "${first_parent_chain}"; then | |
| echo "::error::Source branch '${SOURCE_BRANCH}' is not cut from '${LATEST_TAG}'." | |
| echo "::error::Its first-parent history does not include ${LATEST_TAG_SHA}." | |
| exit 1 | |
| fi | |
| # Additionally, every commit added on top of the tag (the set we are | |
| # about to publish) must itself be a descendant of the tag along | |
| # first-parent — i.e. no sibling commits from master sneak in via a | |
| # non-first-parent path. Enforce by requiring that the symmetric | |
| # difference is empty in one direction: commits in source that are | |
| # NOT first-parent-reachable from source starting at the tag. | |
| # We do this by intersecting: | |
| # A = commits reachable from source but not from tag (full DAG) | |
| # B = commits on the first-parent chain from source down to tag | |
| # and requiring A == B. | |
| all_added="$(git rev-list "${LATEST_TAG_SHA}..${source_sha}" | sort)" | |
| first_parent_added="$( | |
| git rev-list --first-parent "${LATEST_TAG_SHA}..${source_sha}" | sort | |
| )" | |
| if [[ "${all_added}" != "${first_parent_added}" ]]; then | |
| echo "::error::Source branch '${SOURCE_BRANCH}' contains commits not on its first-parent chain from '${LATEST_TAG}'." | |
| echo "::error::This usually means the branch was cut from master (not from the tag) or contains a merge from master." | |
| echo "Commits reachable but not on first-parent chain:" | |
| comm -23 <(printf '%s\n' "${all_added}") <(printf '%s\n' "${first_parent_added}") \ | |
| | while read -r sha; do | |
| echo " $(git log -1 --format='%h %s' "${sha}")" | |
| done | |
| exit 1 | |
| fi | |
| added_count="$(printf '%s\n' "${all_added}" | grep -c . || true)" | |
| echo "Source branch is cut directly from ${LATEST_TAG} with ${added_count} commit(s) on top." | |
| - name: Validate PR exists, is open, named correctly, has latest commit, and checks pass | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} | |
| SOURCE_COMMIT: ${{ inputs.commit }} | |
| NEW_VERSION: ${{ steps.latest.outputs.new_version }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| expected_title="ComfyUI backport release ${NEW_VERSION}" | |
| # Find open PRs from this branch into master. The --state open filter | |
| # is load-bearing: a closed/merged PR with passing checks must not be | |
| # accepted as authorization for a new release. | |
| pr_json="$( | |
| gh pr list \ | |
| --repo "${REPO}" \ | |
| --state open \ | |
| --head "${SOURCE_BRANCH}" \ | |
| --base master \ | |
| --json number,title,headRefOid,state \ | |
| --limit 10 | |
| )" | |
| pr_count="$(echo "${pr_json}" | jq 'length')" | |
| if [[ "${pr_count}" -eq 0 ]]; then | |
| echo "::error::No open PR found from '${SOURCE_BRANCH}' into 'master'. The PR must exist and be open." | |
| exit 1 | |
| fi | |
| # Pick the PR matching the expected title | |
| pr_number="$(echo "${pr_json}" | jq -r --arg t "${expected_title}" ' | |
| map(select(.title == $t)) | .[0].number // empty | |
| ')" | |
| pr_head_sha="$(echo "${pr_json}" | jq -r --arg t "${expected_title}" ' | |
| map(select(.title == $t)) | .[0].headRefOid // empty | |
| ')" | |
| if [[ -z "${pr_number}" ]]; then | |
| echo "::error::No open PR from '${SOURCE_BRANCH}' into 'master' is titled '${expected_title}'." | |
| echo "Found PRs:" | |
| echo "${pr_json}" | jq -r '.[] | " #\(.number): \(.title)"' | |
| exit 1 | |
| fi | |
| # The PR's current head commit must equal the SHA the operator gave us. | |
| # This is what closes the door on releasing stale code: if anyone has | |
| # pushed to the branch since the operator validated tests passed, the | |
| # PR head will have advanced past SOURCE_COMMIT and we abort. (The | |
| # resolve step already proved the branch tip == SOURCE_COMMIT; this | |
| # ties that same SHA to the PR that authorizes the release.) | |
| if [[ "${pr_head_sha}" != "${SOURCE_COMMIT}" ]]; then | |
| echo "::error::PR #${pr_number} head commit is ${pr_head_sha}, but the operator-provided commit is ${SOURCE_COMMIT}." | |
| echo "::error::The PR has new commits since this release was authorized. Re-run with the new head SHA after verifying its checks." | |
| exit 1 | |
| fi | |
| echo "Found open PR #${pr_number} titled '${expected_title}' at head ${pr_head_sha} (matches operator-provided commit)." | |
| # Verify all check runs on the head commit have completed successfully. | |
| # A check is considered passing if conclusion is success, neutral, or skipped. | |
| checks_json="$( | |
| gh api \ | |
| --paginate \ | |
| "repos/${REPO}/commits/${pr_head_sha}/check-runs" \ | |
| --jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}' | |
| )" | |
| if [[ -z "${checks_json}" ]]; then | |
| echo "::error::No check runs found on PR head commit ${pr_head_sha}." | |
| exit 1 | |
| fi | |
| echo "Check runs on ${pr_head_sha}:" | |
| echo "${checks_json}" | jq -s '.' | |
| failing="$(echo "${checks_json}" | jq -s ' | |
| map(select( | |
| .status != "completed" | |
| or (.conclusion as $c | |
| | ["success","neutral","skipped"] | |
| | index($c) | not) | |
| )) | |
| ')" | |
| failing_count="$(echo "${failing}" | jq 'length')" | |
| if [[ "${failing_count}" -gt 0 ]]; then | |
| echo "::error::One or more checks have not passed on PR head commit ${pr_head_sha}:" | |
| echo "${failing}" | jq -r '.[] | " - \(.name): status=\(.status) conclusion=\(.conclusion)"' | |
| exit 1 | |
| fi | |
| echo "All checks have passed on ${pr_head_sha}." | |
| - name: Prepare release branch | |
| id: prepare | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} | |
| LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} | |
| LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }} | |
| PATCH: ${{ steps.latest.outputs.patch }} | |
| run: | | |
| set -euo pipefail | |
| # Try to fetch the release branch. If patch == 0, it shouldn't exist yet | |
| # and we'll create it from the latest stable tag. If patch > 0, it must | |
| # already exist and its tip must equal the latest stable tag commit (i.e. | |
| # the previous patch release). | |
| if git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then | |
| echo "Release branch '${RELEASE_BRANCH}' already exists on origin." | |
| git fetch origin "refs/heads/${RELEASE_BRANCH}:refs/remotes/origin/${RELEASE_BRANCH}" | |
| git checkout -B "${RELEASE_BRANCH}" "refs/remotes/origin/${RELEASE_BRANCH}" | |
| current_tip="$(git rev-parse HEAD)" | |
| if [[ "${current_tip}" != "${LATEST_TAG_SHA}" ]]; then | |
| echo "::error::Release branch '${RELEASE_BRANCH}' tip (${current_tip}) is not at the latest stable release '${LATEST_TAG}' (${LATEST_TAG_SHA})." | |
| echo "::error::Refusing to release on top of a divergent branch." | |
| exit 1 | |
| fi | |
| echo "branch_existed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| if [[ "${PATCH}" != "0" ]]; then | |
| echo "::error::Release branch '${RELEASE_BRANCH}' does not exist on origin, but the latest stable release '${LATEST_TAG}' has patch=${PATCH} (>0). This is inconsistent." | |
| exit 1 | |
| fi | |
| echo "Release branch '${RELEASE_BRANCH}' does not exist. Creating from ${LATEST_TAG}." | |
| git checkout -B "${RELEASE_BRANCH}" "refs/tags/${LATEST_TAG}" | |
| echo "branch_existed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Fast-forward merge source branch into release branch | |
| env: | |
| SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} | |
| SOURCE_COMMIT: ${{ inputs.commit }} | |
| RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} | |
| run: | | |
| set -euo pipefail | |
| # --ff-only guarantees no merge commit is created. If a fast-forward is | |
| # not possible (i.e. the release branch has commits the source branch | |
| # doesn't), the merge will fail and we abort. Because we already validated | |
| # that the source branch is rooted on the latest stable tag, and the | |
| # release branch tip equals that same tag, this fast-forward should | |
| # always succeed for a well-formed backport branch. | |
| # | |
| # We merge the operator-provided SHA, not the branch ref, so a push to | |
| # the branch in the window between resolve and now cannot smuggle new | |
| # commits into the release. | |
| if ! git merge --ff-only "${SOURCE_COMMIT}"; then | |
| echo "::error::Cannot fast-forward '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}'). A merge commit would be required. Aborting." | |
| exit 1 | |
| fi | |
| echo "Fast-forwarded '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}')." | |
| - name: Bump version files | |
| env: | |
| NEW_VERSION_NO_V: ${{ steps.latest.outputs.new_version_no_v }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! -f comfyui_version.py ]]; then | |
| echo "::error::comfyui_version.py not found in repo root." | |
| exit 1 | |
| fi | |
| if [[ ! -f pyproject.toml ]]; then | |
| echo "::error::pyproject.toml not found in repo root." | |
| exit 1 | |
| fi | |
| # Replace the version string in comfyui_version.py. | |
| # Expected format: __version__ = "X.Y.Z" | |
| python3 - "$NEW_VERSION_NO_V" <<'PY' | |
| import re, sys, pathlib | |
| new = sys.argv[1] | |
| p = pathlib.Path("comfyui_version.py") | |
| src = p.read_text() | |
| new_src, n = re.subn( | |
| r'(__version__\s*=\s*[\'"])[^\'"]+([\'"])', | |
| lambda m: f'{m.group(1)}{new}{m.group(2)}', | |
| src, | |
| count=1, | |
| ) | |
| if n != 1: | |
| sys.exit("Could not find __version__ assignment in comfyui_version.py") | |
| p.write_text(new_src) | |
| p = pathlib.Path("pyproject.toml") | |
| src = p.read_text() | |
| # Replace the first `version = "..."` inside [project] or [tool.poetry]. | |
| new_src, n = re.subn( | |
| r'(?m)^(version\s*=\s*")[^"]+(")', | |
| lambda m: f'{m.group(1)}{new}{m.group(2)}', | |
| src, | |
| count=1, | |
| ) | |
| if n != 1: | |
| sys.exit("Could not find version assignment in pyproject.toml") | |
| p.write_text(new_src) | |
| PY | |
| echo "Updated version to ${NEW_VERSION_NO_V} in comfyui_version.py and pyproject.toml." | |
| git --no-pager diff -- comfyui_version.py pyproject.toml | |
| - name: Commit version bump and tag release | |
| env: | |
| NEW_VERSION: ${{ steps.latest.outputs.new_version }} | |
| run: | | |
| set -euo pipefail | |
| git add comfyui_version.py pyproject.toml | |
| git commit -m "ComfyUI ${NEW_VERSION}" | |
| if git rev-parse -q --verify "refs/tags/${NEW_VERSION}" >/dev/null; then | |
| echo "::error::Tag ${NEW_VERSION} already exists locally." | |
| exit 1 | |
| fi | |
| git tag "${NEW_VERSION}" | |
| - name: Verify tag does not already exist on origin | |
| env: | |
| NEW_VERSION: ${{ steps.latest.outputs.new_version }} | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code --tags origin "refs/tags/${NEW_VERSION}" >/dev/null 2>&1; then | |
| echo "::error::Tag ${NEW_VERSION} already exists on origin. Aborting." | |
| exit 1 | |
| fi | |
| - name: Push release branch and tag | |
| env: | |
| RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} | |
| NEW_VERSION: ${{ steps.latest.outputs.new_version }} | |
| run: | | |
| set -euo pipefail | |
| # Push the branch first, then the tag. Atomic-ish: if the branch push | |
| # fails we never publish the tag. | |
| git push origin "refs/heads/${RELEASE_BRANCH}:refs/heads/${RELEASE_BRANCH}" | |
| git push origin "refs/tags/${NEW_VERSION}" | |
| echo "Released ${NEW_VERSION} on ${RELEASE_BRANCH}." | |
| - name: Delete remote source branch | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} | |
| SOURCE_COMMIT: ${{ inputs.commit }} | |
| RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| # Belt-and-braces: the resolve step already refuses the default branch, | |
| # but never delete the default or the release branch under any | |
| # circumstances. | |
| if [[ "${SOURCE_BRANCH}" == "${DEFAULT_BRANCH}" || "${SOURCE_BRANCH}" == "${RELEASE_BRANCH}" ]]; then | |
| echo "::error::Refusing to delete '${SOURCE_BRANCH}' (matches default or release branch)." | |
| exit 1 | |
| fi | |
| # Delete the source branch on origin, but only if its tip is still the | |
| # SHA we released from. If someone pushed new commits to it after we | |
| # resolved it, leave it alone — those commits would be silently lost. | |
| current_tip="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | awk '{print $1}')" | |
| if [[ -z "${current_tip}" ]]; then | |
| echo "Source branch '${SOURCE_BRANCH}' no longer exists on origin; nothing to delete." | |
| exit 0 | |
| fi | |
| if [[ "${current_tip}" != "${SOURCE_COMMIT}" ]]; then | |
| echo "::warning::Source branch '${SOURCE_BRANCH}' tip (${current_tip}) no longer matches released commit (${SOURCE_COMMIT}). Leaving it in place." | |
| exit 0 | |
| fi | |
| git push origin --delete "refs/heads/${SOURCE_BRANCH}" | |
| echo "Deleted remote branch '${SOURCE_BRANCH}'." | |
| - name: Summary | |
| if: always() | |
| env: | |
| NEW_VERSION: ${{ steps.latest.outputs.new_version }} | |
| RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} | |
| LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} | |
| SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} | |
| SOURCE_COMMIT: ${{ inputs.commit }} | |
| run: | | |
| # SOURCE_BRANCH is empty if the resolve step never produced an output | |
| # (e.g. the workflow failed in or before that step). Show a placeholder | |
| # in that case so the summary table still renders cleanly. | |
| source_branch_display="${SOURCE_BRANCH:-(unresolved)}" | |
| { | |
| echo "## Backport release" | |
| echo "" | |
| echo "| Field | Value |" | |
| echo "|---|---|" | |
| echo "| Source commit | \`${SOURCE_COMMIT}\` |" | |
| echo "| Source branch | \`${source_branch_display}\` |" | |
| echo "| Previous stable | \`${LATEST_TAG}\` |" | |
| echo "| New version | \`${NEW_VERSION}\` |" | |
| echo "| Release branch | \`${RELEASE_BRANCH}\` |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |