mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 13:30:55 +02:00
374 lines
15 KiB
YAML
374 lines
15 KiB
YAML
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:-<none>}, 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:-<none>}; 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
|