Files
remove-ai-watermarks/docs/release-and-distribution.md
T

297 lines
16 KiB
Markdown

# Release and distribution
This page describes the release behavior defined in this repository. External
registry state can change independently, so verify it during a release.
## Published surfaces
A release is complete only after all four published surfaces are verified:
| Surface | Published result | Automation |
|---|---|---|
| PyPI | The `remove-ai-watermarks` wheel and source distribution | `publish.yml` |
| Homebrew | The `remove-ai-watermarks` formula in `wiltodelta/homebrew-tap` | `distribute.yml` |
| Hugging Face Space | The Space demo deployed on the new release through a requirements-pin bump in `wiltodelta/raiw-hf-space` | `distribute.yml` dispatches `pin-library.yml` in that repo, waits for the Hub `requirements.txt` pin, then factory-rebuilds; `sync-to-hf.yml` mirrors the pin onto the Space |
| ComfyUI Registry | A compatible release of `wiltodelta/ComfyUI-remove-ai-watermarks` with its own node version | `distribute.yml` and the node repository's workflows |
The GitHub Release is the trigger and release record for this flow. Conda is
not a supported publishing surface: this repository has no conda recipe or
conda publication job.
## Release sources of truth
The package version appears in:
- `pyproject.toml`;
- `src/remove_ai_watermarks/__init__.py`;
- the root package entry generated in `uv.lock`.
Update the first two, then refresh the lock file with uv. Do not edit a
line-number-specific location in `uv.lock`; its package order changes.
Before choosing a version, verify it is absent from git tags, GitHub Releases,
and PyPI. An unpublished remote tag still reserves that version; never move it,
publish the next version instead.
## Publish flow
PyPI publishing is triggered by a published GitHub Release, not by a tag push
alone.
The expected sequence:
1. update the version sources and lock file;
2. run the complete project gate;
3. commit the release change;
4. create an annotated `vX.Y.Z` tag;
5. push the commit and tag;
6. publish the GitHub Release.
`.github/workflows/publish.yml` then:
1. checks that the release tag matches `pyproject.toml`;
2. builds the package with uv;
3. publishes with `uv publish` through PyPI trusted publishing.
The workflow uses GitHub OIDC through the `pypi` environment. It does not read a
PyPI API token from the repository.
## Post-release distribution
`.github/workflows/distribute.yml` runs on the same published-release event. It
waits for the matching source distribution to appear on PyPI, then:
- updates the Homebrew tap formula URL and SHA-256;
- bumps the Hugging Face Space pin and factory-rebuilds the demo on that pin;
- synchronizes, tests, versions, and publishes the ComfyUI nodes.
Upgrading the Hugging Face Space is dispatched from `distribute.yml`: it runs
`pin-library.yml` in `wiltodelta/raiw-hf-space`, which bumps
`remove-ai-watermarks[visible,heif]` in `pyproject.toml`, refreshes `uv.lock`,
and pushes. That repository's `sync-to-hf.yml` then mirrors the files onto the
Space. `distribute.yml` waits for the Hub `requirements.txt` pin to match,
then factory-rebuilds. The `RAIW_HF_SPACE_TOKEN` repository secret is a
fine-grained token limited to the Space repository, with Contents write and
Actions write. Without the secret the pin bump is skipped and only the
factory rebuild runs, which reinstalls whatever version is already pinned.
The Space repo's callback smoke tests still run on every push; they are the
scenario check for the demo surface.
The Hugging Face **model** `wiltodelta/raiw-photo-classify` is not part of that
release fan-out. It holds the photo-classify freeze weights. The library extra
`classify` downloads that snapshot on first use, or reads
`RAIW_CLASSIFY_WEIGHTS`. Update the Hub repo only with
`.github/workflows/publish-photo-classify-hf.yml` (`workflow_dispatch`,
type `publish`). Full mode downloads the four weight files from the GitHub
Release tag `photo-classify-freeze-2026-08-31` (or the tag you pass), exports
the static FP32 vision-only ONNX graph from `clip-l-ft.pt`, and uploads all five
files with the card in `docs/photo-classify-hf/`. Card mode updates
the README, `operating-point.json`, and the receipt-gate head staged from
the package assets; freeze weights are untouched. A library version bump
alone does not upload new heads -- bump the pinned Hub revision when a new
head must reach installed clients. Pin a Hub revision in production so a
later freeze cannot silently change installed clients.
The separate `wiltodelta/raiw-source-classify` model is published with
`.github/workflows/publish-source-classify-hf.yml`. The workflow downloads the
derivative `source-pipeline-mlp.npz` artifact from the declared GitHub model
freeze, verifies its SHA-256, and uploads only that file plus the tracked card
and aggregate metrics under `docs/source-classify-hf/`. The private corpus,
catalog, paths, hashes, embeddings, and feature caches are outside this flow.
The workflow is manual and requires the model-freeze release tag plus an
explicit `publish` confirmation. Keep model-freeze GitHub releases marked as
pre-releases so a model artifact cannot replace the latest package release in
GitHub's release UI. Package publication and distribution jobs run only for
non-prerelease `v*` tags, so model-only prereleases cannot enter PyPI or the
package fan-out. A draft release keeps its target commit when published; target
the commit containing those guards before creating a model-only release, not
merely the then-current `main` after the draft already exists.
The newer `wiltodelta/openai-google-image-source-classifier` uses the separate
`publish-image-source-hf.yml` workflow. Its release asset is only the
hash-verified derivative `openai-google-source-v1.npz`; the workflow uploads
that artifact plus `docs/image-source-hf/README.md` and `metrics.json`. The
training corpus does not enter the library checkout or this release.
If a distribution job fails because a repository or Hugging Face credential is
invalid, rotate the corresponding GitHub secret and rerun the failed job. A
manual Homebrew formula update is the fallback when its automation is blocked.
## Source distribution boundary
The wheel includes the package under `src/`.
The source distribution uses an explicit allowlist for `/src`, `/LICENSE`,
`/README.md`, and `/pyproject.toml` through
`[tool.hatch.build.targets.sdist]` in `pyproject.toml`. It also defensively
excludes `/data`, `/tmp`, and `/.sc`. Keep both controls: calibration captures,
test corpora, generated research outputs, and local session state do not belong
in the published package archive. Hatchling always adds the root `.gitignore` to
the sdist, so keep its comments generic and free of local operational context.
Ignore rules are not the build boundary: `data/` is deliberately tracked, while
the sdist configuration keeps it and the other excluded paths out of the archive.
## Build backend
The package uses hatchling through the unpinned `hatchling` build requirement in
`pyproject.toml`. Uploading uses uv rather than the older twine-based action.
## Other channels
The ComfyUI nodes are maintained and versioned in their own repository. After
the matching source distribution appears on PyPI, `distribute.yml` dispatches
that repository's sync workflow with the exact library version and waits for it
to finish. The sync updates the dependency floor, runs compatibility tests,
bumps the node patch version, and publishes to the ComfyUI Registry only when
those tests pass. Its daily schedule remains as a recovery path if a release
dispatch is interrupted. The `COMFYUI_RELEASE_TOKEN` repository secret is a
fine-grained token limited to the ComfyUI node repository, with Actions read and
write access.
## Agent skill
The Agent Skill and Claude plugin live in this repository:
- [`skills/remove-ai-watermarks/`](../skills/remove-ai-watermarks/) is the
portable Agent Skills package;
- [`skills/.claude-plugin/plugin.json`](../skills/.claude-plugin/plugin.json)
is the Claude plugin manifest, with source `./skills` so an install copies
only the skill tree;
- [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json)
makes the repository a Claude Code marketplace.
They are not a fifth required release surface. A CLI change that affects
command routing, extras, exit codes, mark keys, or the intended-use boundary
must update the skill in the same commit. User-facing install steps live in
[`docs/agent-skill.md`](agent-skill.md).
After the skill files reach the default branch:
- `npx skills add wiltodelta/remove-ai-watermarks` installs from GitHub;
- SkillsMP can index the public `SKILL.md`;
- Claude Code users can run `/plugin marketplace add wiltodelta/remove-ai-watermarks`.
ClawHub publishes automatically: the `clawhub` job in `distribute.yml` compares
the `SKILL.md` version with the published one on every GitHub Release and
submits an update when they differ. After its local npm installation, the
job invokes `./node_modules/.bin/clawhub` explicitly. It needs the
`CLAWHUB_TOKEN` secret
(create a dedicated token on clawhub.ai, not a device-flow one); without
the secret the job skips with a notice. A submission waits for ClawHub
security scans before the listing goes public, so a green job means
"submitted", not "listed": verify the live catalog before calling the
surface complete. The Claude community marketplace remains a manual,
authenticated publish. Do not treat any listing as complete until the live
catalog shows the skill.
When `metadata.version` in `SKILL.md` changes, finish the catalog follow-up as
part of the release that carries that version:
1. Verify the automatic ClawHub submission and then the live version. Do not
publish it a second time by hand.
2. If the Claude community directory listing is live, publish the updated
plugin through its authenticated submission surface and verify the listing.
If the original submission is still under review, leave it pending rather
than opening a duplicate.
3. If the AI Skills Catalog listing is live, send the new skill version through
the update route supplied with its acceptance. The catalog stores compatible
packages as well as source metadata, so a GitHub push alone is not a verified
update. If no update control was supplied, contact the catalog or resubmit
and verify the resulting listing. Do not duplicate a pending initial review.
4. If the OpenAI plugin directory listing has been approved, upload the new
plugin archive through the Platform portal and verify the published version.
Until the initial product-specific review is approved, there is no release
action beyond tracking that review.
5. SkillsMP, skills.sh, AgenticSkills, and the awesome lists point at GitHub or
refresh from it. Do not resubmit a routine version bump. Contact them or open
a new PR only when their public description, category, tags, or repository
URL must change.
The automated release workflow cannot perform the authenticated directory
steps. A release with a changed skill version is not fully distributed until
each applicable manual step is verified or explicitly marked pending because
the catalog has not accepted the initial submission. The catalog-specific
submission state and links live in
[`docs/agent-skill.md`](agent-skill.md#where-the-skill-can-actually-be-listed).
## Release verification
Forensic transports are versioned independently from the package. Before publishing
a change to provenance metadata, provenance reports, broad forensic metadata, or
pixel evidence, run their schema 1 contract tests. Additive fields are compatible;
renaming a field, changing its type or meaning, changing a signal name or watermark
label, or removing a field requires a new output schema. Add the new serializer
without removing schema 1 so long-lived consumers can update separately. A package
release must never silently substitute its latest schema when a caller explicitly
requests an older supported one.
After publication, the `verify-release.yml` workflow runs automatically once
`distribute.yml` completes and checks every surface below. The automatic
run downloads `distributed-release-*` from the exact triggering distribution
run, verifies the run ID and artifact attempt, and selects the highest recorded
attempt no later than the triggering attempt. This also handles reruns of
failed jobs that reuse an earlier successful version-resolution job. A newer
PyPI release cannot silently change the recorded target.
Manual dispatch accepts an explicit version, or uses PyPI latest when omitted.
Every check fails the job; none of
them warns and continues, because a surface nobody can confirm is not a verified
surface, with one deliberate exception noted below. The ComfyUI check reads the
registry's JSON API and asserts that some published node version declares this
library release. Dependencies are parsed as packaging requirements: the
canonical package name must be `remove-ai-watermarks`, an exact `>=` floor
must name the target release, and the full constraint must admit it. URL
and environment-conditional requirements do not satisfy that contract.
It deliberately does NOT proxy through the sync workflow's run
status: the node repository also runs that workflow on a daily schedule, so a
green nightly run says nothing about whether this release was synced.
Read `/nodes/remove-ai-watermarks/versions`, not `latest_version` on the node
object. The registry reviews an upload before activating it, and
`latest_version` reports only the active version, so during that review window a
correctly published version is indistinguishable from one that was never
uploaded. A new version therefore sits in `NodeVersionStatusPending` for a while
after a release; the check reports that state in the run summary and passes,
because it is the registry's queue rather than an incomplete release. A missing
version, or any other status, fails. The full checklist for completeness:
- both wheel and source distribution exist on PyPI;
- the package version matches the tag;
- the Homebrew formula points to the new source distribution;
- the distribution workflow completed successfully;
- the ComfyUI Registry node requires the new library version;
- the Hugging Face Space serves the new release: its `requirements.txt` pin
matches (raw file via the HF API), the runtime stage is `RUNNING` after the
rebuild, and one live `identify` and one live `visible` API call succeed
against the running Space;
- a clean install can run `remove-ai-watermarks --version`.
- when the Agent Skill version changed, every applicable manual catalog update
above is verified, or its initial submission remains pending without a
duplicate submission.
A clean-install check run immediately after publication can fail with "no
version of remove-ai-watermarks==X.Y.Z" even though the simple index already
lists it: uv serves its cached pre-release view of the index. Re-run with a
fresh cache (`UV_CACHE_DIR=$(mktemp -d)`) before treating it as a release
failure.
The ComfyUI sync resolves the version from the PyPI JSON API but installs its
test dependency through pip's simple index, whose CDN edges can lag by
minutes. `distribute.yml` gates every downstream job on both APIs listing the
release, so it cannot dispatch that race again. The ComfyUI waiter itself has
a clock: 0.40.0's node suite finished after the 15-minute poll, so it now
waits 30 minutes and then accepts a registry row that already requires this
library version. For an older distribution that started before the simple
index caught up, rerun its failed `comfyui` job only when the registry still
has no such row.
The automatic `verify-release.yml` run after `distribute.yml` checks the
Hugging Face Space pin. When `RAIW_HF_SPACE_TOKEN` is set, that pin lands
during distribution. Without the secret, bump the pin by dispatching
`pin-library.yml` in `wiltodelta/raiw-hf-space` and then dispatch
`verify-release.yml` with the release version. Do not read a missing Space
pin as a broken PyPI or Homebrew publish.
If the release changed CLI routing, extras, exit codes, mark keys, or the
intended-use boundary, confirm `skills/remove-ai-watermarks/` still matches.