name: Distribute on release # Fans a published GitHub Release out to the channels that need a nudge. # PyPI is handled by publish.yml. This workflow event-drives the four channels # that would otherwise be manual: # - Homebrew tap: rewrite the formula's url + sha256 to the new sdist. # - HF Space: factory-rebuild the version already pinned in the Space repo. # - ComfyUI: sync, test, and publish the node against the exact library release. # - ClawHub: publish the agent skill when its own version moved. # All wait for the freshly published distribution to appear in both PyPI APIs # first, since the Release event fires in parallel with publish.yml's upload. on: release: types: [published] workflow_dispatch: inputs: version: description: "Version to distribute (no leading v), e.g. 0.10.3. Blank = latest on PyPI." required: false jobs: resolve: # Model-only prereleases must not reach PyPI, Homebrew, Spaces, or ComfyUI. if: ${{ github.event_name != 'release' || (startsWith(github.event.release.tag_name, 'v') && !github.event.release.prerelease) }} runs-on: ubuntu-latest # Thirty attempts may spend up to 20 seconds each in the JSON request, # simple-index request, and inter-attempt delay. Leave bounded headroom. timeout-minutes: 25 outputs: version: ${{ steps.v.outputs.version }} url: ${{ steps.sdist.outputs.url }} sha: ${{ steps.sdist.outputs.sha }} steps: - name: Resolve target version id: v run: | set -euo pipefail if [ "${{ github.event_name }}" = "release" ]; then v="${GITHUB_REF_NAME#v}" elif [ -n "${{ github.event.inputs.version }}" ]; then v="${{ github.event.inputs.version }}" else v=$(curl -fsSL https://pypi.org/pypi/remove-ai-watermarks/json \ | python3 -c "import sys,json;print(json.load(sys.stdin)['info']['version'])") fi echo "version=$v" >> "$GITHUB_OUTPUT" echo "Target version: $v" - name: Record distributed release env: VERSION: ${{ steps.v.outputs.version }} run: | python3 - <<'PYTHON' import json, os from pathlib import Path Path("release-identity.json").write_text(json.dumps({ "version": os.environ["VERSION"], "run_id": os.environ["GITHUB_RUN_ID"], "run_attempt": os.environ["GITHUB_RUN_ATTEMPT"], }), encoding="utf-8") PYTHON - name: Upload distributed release identity uses: actions/upload-artifact@v7 with: name: distributed-release-${{ github.run_attempt }} path: release-identity.json if-no-files-found: error - name: Wait for the PyPI JSON and simple indexes, capture sdist metadata id: sdist run: | set -euo pipefail v="${{ steps.v.outputs.version }}" for i in $(seq 1 30); do if rel=$(curl --connect-timeout 10 --max-time 20 -fsSL "https://pypi.org/pypi/remove-ai-watermarks/$v/json" 2>/dev/null); then read -r url sha <<< "$(echo "$rel" | python3 -c "import sys,json;u=next((u for u in json.load(sys.stdin)['urls'] if u['packagetype']=='sdist'),{});print(u.get('url',''),u.get('digests',{}).get('sha256',''))")" if [ -n "$url" ] && [ -n "$sha" ]; then # PyPI's JSON API can show a freshly uploaded file before the # simple index used by pip has caught up. Downstream release # jobs install by package version, so waiting on JSON alone # creates a release-time race for them. simple=$(curl --connect-timeout 10 --max-time 20 -fsSL https://pypi.org/simple/remove-ai-watermarks/ 2>/dev/null || true) artifact=${url##*/} if [[ "$simple" == *"$artifact"* ]]; then echo "url=$url" >> "$GITHUB_OUTPUT" echo "sha=$sha" >> "$GITHUB_OUTPUT" echo "PyPI JSON and simple indexes list $v" exit 0 fi fi fi if [ "$i" -lt 30 ]; then echo "PyPI indexes do not both list $v yet (attempt $i), waiting..." sleep 20 fi done echo "::error::PyPI JSON and simple indexes did not both list $v in time" exit 1 homebrew: needs: resolve runs-on: ubuntu-latest steps: - name: Checkout the tap uses: actions/checkout@v7 with: repository: wiltodelta/homebrew-tap token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - name: Bump the formula env: URL: ${{ needs.resolve.outputs.url }} SHA: ${{ needs.resolve.outputs.sha }} VERSION: ${{ needs.resolve.outputs.version }} run: | set -euo pipefail F=Formula/remove-ai-watermarks.rb sed -i -E "s|url \"[^\"]*\"|url \"$URL\"|" "$F" sed -i -E "s|sha256 \"[^\"]*\"|sha256 \"$SHA\"|" "$F" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add "$F" git commit -m "remove-ai-watermarks $VERSION" || { echo "Formula already current"; exit 0; } git push hf-space: needs: resolve runs-on: ubuntu-latest steps: - name: Bump the Space pin, then factory-rebuild env: GH_TOKEN: ${{ secrets.RAIW_HF_SPACE_TOKEN }} HF_TOKEN: ${{ secrets.HF_TOKEN }} VERSION: ${{ needs.resolve.outputs.version }} run: | set -euo pipefail repo=wiltodelta/raiw-hf-space workflow=pin-library.yml if [ -z "${GH_TOKEN:-}" ]; then echo "RAIW_HF_SPACE_TOKEN secret is not set; skipping the pin bump." else dispatched_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) gh workflow run "$workflow" \ --repo "$repo" \ --ref main \ --raw-field library_version="$VERSION" run_id="" for attempt in $(seq 1 30); do run_id=$( gh run list \ --repo "$repo" \ --workflow "$workflow" \ --event workflow_dispatch \ --limit 10 \ --json databaseId,createdAt \ | python3 -c "import json,sys; t=sys.argv[1]; print(next((r['databaseId'] for r in json.load(sys.stdin) if r['createdAt']>=t), ''))" \ "$dispatched_at" ) if [ -n "$run_id" ]; then break fi echo "Space pin run not visible yet (attempt $attempt), waiting..." sleep 2 done if [ -z "$run_id" ]; then echo "::error::The dispatched Space pin run did not appear" exit 1 fi for attempt in $(seq 1 180); do read -r status conclusion <<< "$( gh run view "$run_id" \ --repo "$repo" \ --json status,conclusion \ --jq '[.status, (.conclusion // "")] | @tsv' )" if [ "$status" = "completed" ]; then if [ "$conclusion" = "success" ]; then echo "Space pin run $run_id succeeded" break fi echo "::error::Space pin run $run_id completed with $conclusion" exit 1 fi echo "Space pin run $run_id is $status (attempt $attempt), waiting..." sleep 5 done if [ "${status:-}" != "completed" ]; then echo "::error::Space pin run $run_id did not finish in time" exit 1 fi echo "Waiting for the Space requirements pin to show $VERSION..." pinned="" for attempt in $(seq 1 40); do reqs=$(curl -fsSL \ "https://huggingface.co/spaces/wiltodelta/remove-ai-watermarks/raw/main/requirements.txt" \ || true) pinned=$(printf '%s\n' "$reqs" \ | sed -n 's/^remove-ai-watermarks==\([0-9][0-9a-zA-Z.]*\).*/\1/p' \ | head -1) echo " attempt $attempt: Space pin=$pinned" if [ "$pinned" = "$VERSION" ]; then break fi sleep 15 done if [ "$pinned" != "$VERSION" ]; then echo "::error::Space requirements.txt still pinned to ${pinned:-}, not $VERSION" exit 1 fi fi python3 -m pip install --quiet huggingface_hub python3 - <<'PY' import os from huggingface_hub import HfApi HfApi(token=os.environ["HF_TOKEN"]).restart_space( "wiltodelta/remove-ai-watermarks", factory_reboot=True ) print("HF Space factory rebuild triggered") PY clawhub: needs: resolve runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Publish the agent skill when its version moved env: CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} run: | set -euo pipefail if [ -z "${CLAWHUB_TOKEN:-}" ]; then echo "CLAWHUB_TOKEN secret is not set; skipping ClawHub publish." exit 0 fi npm install --silent --no-fund --no-audit clawhub skill_version=$(python3 - <<'PY' import re text = open("skills/remove-ai-watermarks/SKILL.md", encoding="utf-8").read() m = re.search(r"(?m)^ version:\s*[\"']?([^\"'\s]+)", text) assert m, "skill version missing" print(m.group(1)) PY ) ./node_modules/.bin/clawhub login --token "$CLAWHUB_TOKEN" >/dev/null published=$(./node_modules/.bin/clawhub inspect remove-ai-watermarks --file SKILL.md 2>/dev/null \ | python3 -c 'import re,sys; t=sys.stdin.read(); m=re.search(r"(?m)^ version:\s*[\"'"'"']?([^\"'"'"'\s]+)", t); print(m.group(1) if m else "")' || true) if [ "$published" = "$skill_version" ]; then echo "ClawHub already at $skill_version; nothing to publish." exit 0 fi echo "ClawHub at ${published:-}; publishing skill $skill_version." ./node_modules/.bin/clawhub skill publish skills/remove-ai-watermarks \ --slug remove-ai-watermarks \ --name "Remove AI Watermarks" \ --version "$skill_version" \ --changelog "Agent skill sync for the ${{ github.event.release.name || github.ref_name }} release." echo "Submitted; ClawHub runs security scans before the listing goes public." comfyui: needs: resolve runs-on: ubuntu-latest steps: - name: Sync and publish the ComfyUI nodes env: GH_TOKEN: ${{ secrets.COMFYUI_RELEASE_TOKEN }} run: | set -euo pipefail repo=wiltodelta/ComfyUI-remove-ai-watermarks workflow=sync-library.yml dispatched_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) gh workflow run "$workflow" \ --repo "$repo" \ --ref main \ --raw-field library_version="${{ needs.resolve.outputs.version }}" run_id="" for attempt in $(seq 1 30); do run_id=$( gh run list \ --repo "$repo" \ --workflow "$workflow" \ --event workflow_dispatch \ --limit 10 \ --json databaseId,createdAt \ | python3 -c ' import json import sys dispatched_at = sys.argv[1] runs = json.load(sys.stdin) print(next( (run["databaseId"] for run in runs if run["createdAt"] >= dispatched_at), "", )) ' "$dispatched_at" ) if [ -n "$run_id" ]; then break fi echo "ComfyUI sync run not visible yet (attempt $attempt), waiting..." sleep 2 done if [ -z "$run_id" ]; then echo "::error::The dispatched ComfyUI sync run did not appear" exit 1 fi # 0.40.0's node sync finished after this waiter: 180 * 5s is 15 min, # and the node suite plus registry upload ran longer. Keep the # workflow wait, then accept a registry row for this library version # so a late-but-published node is not reported as a missing release. version="${{ needs.resolve.outputs.version }}" for attempt in $(seq 1 360); do read -r status conclusion <<< "$( gh run view "$run_id" \ --repo "$repo" \ --json status,conclusion \ --jq '[.status, (.conclusion // "")] | @tsv' )" if [ "$status" = "completed" ]; then if [ "$conclusion" = "success" ]; then echo "ComfyUI sync run $run_id succeeded" exit 0 fi echo "::error::ComfyUI sync run $run_id completed with $conclusion" exit 1 fi echo "ComfyUI sync run $run_id is $status (attempt $attempt), waiting..." sleep 5 done echo "ComfyUI sync run $run_id did not finish in time; checking the registry." python3 -m pip install --quiet packaging if curl -fsSL "https://api.comfy.org/nodes/remove-ai-watermarks/versions" \ | VERSION="$version" python3 -c ' import json, os, sys from packaging.requirements import InvalidRequirement, Requirement from packaging.utils import canonicalize_name from packaging.version import Version version = os.environ["VERSION"] target = Version(version) def requires_release(value): try: requirement = Requirement(value) except InvalidRequirement: return False return ( canonicalize_name(requirement.name) == "remove-ai-watermarks" and requirement.url is None and requirement.marker is None and target in requirement.specifier and any(s.operator == ">=" and Version(s.version) == target for s in requirement.specifier) ) matches = [ v for v in json.load(sys.stdin) if any(requires_release(d) for d in (v.get("dependencies") or [])) ] if not matches: sys.exit(1) newest = max(matches, key=lambda v: v.get("createdAt") or "") print("%s %s" % (newest.get("version"), newest.get("status"))) '; then echo "Registry already lists a node for $version; the wait timed out after publish." exit 0 fi echo "::error::ComfyUI sync run $run_id did not finish in time and the registry has no node for $version" exit 1