Files
remove-ai-watermarks/.github/workflows/verify-release.yml
T
Victor KuznetsovandClaude Opus 5 326840bab4 Make the release verification actually verify the Space and the node
The HF Space step curled the root for HTTP 200 and then emitted a warning
and SUCCEEDED when it never got one. The documented contract was three other
checks entirely: the requirements pin, the runtime stage, and live identify
and visible calls. None was implemented, which is why the demo sat two
releases behind while every verification run stayed green. Implemented all
three; the pin check fails today, correctly, on 0.34.1 against 0.36.0.

The ComfyUI step read the latest run of the node repo's sync workflow. That
workflow also runs on a daily schedule, so a green nightly satisfied it
whether or not this release was ever synced. The registry does publish a JSON
API, contrary to the comment that justified the proxy, so read the node
directly and assert the active version declares this library release.

Both of those steps warned and continued; only Homebrew failed hard. Now all
of them exit non-zero, and the summary is assembled from lines each step
appends as it passes rather than an unconditional block that claimed five
verified surfaces regardless.

No step needs the GitHub token any more, so pin least-privilege permissions
here and in test.yml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 15:33:11 -07:00

224 lines
10 KiB
YAML

name: Verify release surfaces
# Post-release verification: after distribute.yml fans a release out to
# Homebrew, the HF Space, and the ComfyUI Registry, confirm every published
# surface actually landed and works. This workflow is the automated twin of
# the manual checklist in docs/release-and-distribution.md ("Release
# verification"), not a replacement for the library's own test suite (which
# already ran in test.yml and publish.yml).
on:
workflow_dispatch:
inputs:
version:
description: "Version to verify (no leading v). Blank = latest on PyPI."
required: false
workflow_run:
workflows: ["Distribute on release"]
types: [completed]
# No step reads the GitHub token any more: the ComfyUI check moved to the
# registry's public API. Keep the job read-only so it cannot write on a repo
# whose default token permission is read/write.
permissions:
contents: read
jobs:
verify:
# Only run on a successful distribute (not on manual dispatch, where the
# version input or PyPI-latest provides the target).
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 25
env:
# The Space id appears in the Hub API, the raw-file URL and the app host,
# spelled three different ways. Name it once so a move cannot half-land.
SPACE_REPO: wiltodelta/remove-ai-watermarks
SPACE_URL: https://wiltodelta-remove-ai-watermarks.hf.space
steps:
- name: Resolve target version
id: v
run: |
set -euo pipefail
if [ -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 "Verifying release $v"
# -- PyPI ---------------------------------------------------------------
- name: Verify PyPI
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
set -euo pipefail
echo "Checking PyPI for $VERSION..."
rel=$(curl -fsSL "https://pypi.org/pypi/remove-ai-watermarks/$VERSION/json")
echo "$rel" | python3 -c "
import json, sys
d = json.load(sys.stdin)
assert d['info']['version'] == '$VERSION', f\"PyPI version mismatch: {d['info']['version']}\"
assert d['urls'], 'No files on PyPI'
has_wheel = any(u['packagetype'] == 'bdist_wheel' for u in d['urls'])
has_sdist = any(u['packagetype'] == 'sdist' for u in d['urls'])
assert has_wheel, 'No wheel on PyPI'
assert has_sdist, 'No sdist on PyPI'
print(f' PyPI: {len(d[\"urls\"])} files (wheel + sdist) for $VERSION')
"
# -- Clean install from PyPI ---------------------------------------------
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Verify clean install
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
set -euo pipefail
echo "Installing remove-ai-watermarks==$VERSION in a clean environment..."
export PATH="$HOME/.local/bin:$PATH"
result=$(uvx --from "remove-ai-watermarks==$VERSION" remove-ai-watermarks --version 2>&1) \
|| { echo "::error::Clean install failed: $result"; exit 1; }
echo " Clean install: $result"
# -- Homebrew --------------------------------------------------------------
- name: Verify Homebrew formula
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
set -euo pipefail
echo "Checking Homebrew tap formula..."
formula=$(curl -fsSL \
"https://raw.githubusercontent.com/wiltodelta/homebrew-tap/main/Formula/remove-ai-watermarks.rb")
echo "$formula" | grep -q "remove_ai_watermarks-$VERSION.tar.gz" \
|| { echo "::error::Homebrew formula does not reference $VERSION"; \
echo "Formula URL line:"; echo "$formula" | grep '^url'; exit 1; }
echo " Homebrew: formula points to $VERSION"
# -- ComfyUI ------------------------------------------------------------------
- name: Verify ComfyUI registry node requires this release
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
set -euo pipefail
echo "Checking the ComfyUI Registry node..."
# The registry publishes the node as JSON, including the dependency the
# published version declares. Read that directly instead of proxying
# through the sync workflow's run status: the node repo runs
# sync-library.yml on a daily schedule too, so a green nightly run says
# nothing about whether THIS release was ever synced.
node=$(curl -fsSL "https://api.comfy.org/nodes/remove-ai-watermarks")
echo "$node" | VERSION="$VERSION" python3 -c "
import json, os, sys
version = os.environ['VERSION']
latest = json.load(sys.stdin).get('latest_version') or {}
deps = latest.get('dependencies') or []
floor = f'>={version}'
if latest.get('status') != 'NodeVersionStatusActive':
sys.exit(f\"::error::ComfyUI node {latest.get('version')} is {latest.get('status')}, not active\")
if not any(floor in d for d in deps):
sys.exit(
f'::error::ComfyUI node {latest.get(\"version\")} does not require '
f'{version}; it declares {deps}'
)
print(f' ComfyUI: node {latest[\"version\"]} requires {version}')
"
echo "- ComfyUI: registry node requires $VERSION" >> "$GITHUB_STEP_SUMMARY"
# -- HF Space -------------------------------------------------------------------
- name: Verify the HF Space serves this release
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
set -euo pipefail
echo "Checking the HF Space pin..."
# The Space pin bump is a MANUAL step (see docs/release-and-distribution.md):
# distribute.yml only factory-rebuilds whatever is already pinned. Failing
# here is the point -- a demo left on an older release is an incomplete
# release, and only this check can say so.
reqs=$(curl -fsSL "https://huggingface.co/spaces/$SPACE_REPO/raw/main/requirements.txt")
pinned=$(printf '%s\n' "$reqs" | sed -n 's/^remove-ai-watermarks==\([0-9][0-9a-zA-Z.]*\).*/\1/p' | head -1)
if [ -z "$pinned" ]; then
echo "::error::Space requirements.txt has no remove-ai-watermarks== pin"
exit 1
fi
if [ "$pinned" != "$VERSION" ]; then
echo "::error::HF Space is pinned to $pinned, not $VERSION."
echo " Bump remove-ai-watermarks in wiltodelta/raiw-hf-space (pyproject.toml,"
echo " uv lock, re-exported requirements.txt) and push; sync-to-hf.yml mirrors it."
exit 1
fi
echo " HF Space: requirements pin is $pinned"
- name: Verify the HF Space is running and answering
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
set -euo pipefail
# A factory rebuild leaves the Space BUILDING for a while; wait for the
# runtime stage the API reports rather than for a bare HTTP 200, which a
# build page also returns.
for attempt in $(seq 1 20); do
stage=$(curl -fsSL "https://huggingface.co/api/spaces/$SPACE_REPO" \
| python3 -c "import json,sys;print((json.load(sys.stdin).get('runtime') or {}).get('stage',''))")
echo " attempt $attempt: runtime stage=$stage"
[ "$stage" = "RUNNING" ] && break
case "$stage" in
BUILD_ERROR|RUNTIME_ERROR|CONFIG_ERROR)
echo "::error::HF Space runtime stage is $stage"; exit 1 ;;
esac
sleep 15
done
if [ "$stage" != "RUNNING" ]; then
echo "::error::HF Space never reached RUNNING (last stage: $stage)"
exit 1
fi
# Alive is not the same as working. Drive the two CPU surfaces the demo
# exists for, against the Space's own committed example, and require a
# real verdict rather than any HTTP 200.
export PATH="$HOME/.local/bin:$PATH"
curl -fsSL -o /tmp/example.png \
"https://huggingface.co/spaces/$SPACE_REPO/resolve/main/examples/doubao-aigc.png"
uv run --no-project --with 'gradio_client>=1,<2' python - <<'PYEOF'
import os
import sys
from gradio_client import Client, handle_file
# The committed example is a 5 MB PNG, and httpx's default 5 s write
# timeout aborts that upload on a slow link before the Space ever sees
# it -- which would read as a broken demo rather than a slow one.
client = Client(os.environ["SPACE_URL"], verbose=False, httpx_kwargs={"timeout": 300})
report = client.predict(path=handle_file("/tmp/example.png"), api_name="/run_identify")
if "Verdict:" not in report:
sys.exit(f"::error::/run_identify returned no verdict: {report[:200]!r}")
print(f" HF Space identify: {report.splitlines()[0]}")
_image, _file, info = client.predict(
path=handle_file("/tmp/example.png"),
sensitivity_label="auto (recommended)",
api_name="/run_visible",
)
if "Removed:" not in info:
sys.exit(f"::error::/run_visible removed nothing on the Doubao example: {info!r}")
print(f" HF Space visible: {info.splitlines()[0]}")
PYEOF
echo "- HF Space: pinned to $VERSION, RUNNING, identify + visible answered" >> "$GITHUB_STEP_SUMMARY"
- name: Summary
env:
VERSION: ${{ steps.v.outputs.version }}
run: |
# Every check above appends its own line to the step summary as it passes,
# and every one of them fails the job instead of warning. So this heading
# can only be reached when all of them actually held -- an unconditional
# "verified" block printed here once claimed surfaces nobody had checked.
sed -i "1i ## Release $VERSION verified" "$GITHUB_STEP_SUMMARY"
cat "$GITHUB_STEP_SUMMARY"