Maintainer notes. Nothing here is needed to use the package.
Two registries, two independent version lines:
| Package | Version lives in | |
|---|---|---|
| PyPI | barber-llm |
pyproject.toml, barber/__init__.py |
| npm | barber-llm |
js/package.json, js/index.js (VERSION) |
PyPI and npm are not meant to agree with each other and no release is ever
cut to make them agree. They ship different surfaces — sweep(), the semantic
embedders and the eval harness are Python-only — so PyPI runs ahead. Within one
language the two files must always agree; that is what
python tests/test_versions.py enforces, in CI and in release.yml's test job.
0.4.0 shipped to PyPI reporting barber.__version__ == "0.3.1", and its CLI
printed 0.2.1, because the bump touched only the two manifests. A PyPI upload
cannot be replaced or re-uploaded — yanking does not free the number — so the
only remedy was 0.4.1, a patch release that fixed nothing but its own version
string.
python tests/gen_golden.py --check does not cover this, despite sounding
like it might. It stamps barber.__version__ into the golden fixture and then
compares against a fresh run, so a stale __version__ regenerates a
stale-in-the-same-way fixture and the check passes. It gates selection
decisions, not versions.
Run this before every release, manual or automated:
python tests/test_versions.pyIt names the disagreeing file and prints both values.
- Bump all four locations in one commit. Python and JS bump independently:
a Python-only change bumps the two Python files and leaves
js/alone. python tests/test_versions.py && pytest tests/ -q && python tests/gen_golden.py --checkcd js && node --test- Commit, push to
main, wait forci.ymlto be green. - Tag
vX.Y.Zand push the tag. That triggersrelease.yml, which re-runs the suite against the tagged tree and then publishes — see below for why the publish half currently does not work. - npm is separate and manual:
cd js && npm publish(see Publishing to npm).
release.yml is written for Trusted Publishing (OIDC): GitHub proves the
workflow's identity to PyPI directly, so no API token exists anywhere to store,
paste, leak or rotate.
The trusted publisher has never been registered on PyPI. Until it is, the publish step cannot work and every release goes up by hand.
Only the repo owner can register it — it is an account action on PyPI, not something that can be done from this repository or by anyone without owner rights on the project. At https://pypi.org/manage/project/barber-llm/settings/publishing/, add a trusted publisher to the existing project:
| Field | Value |
|---|---|
| Owner | NadirRouter |
| Repository | barber |
| Workflow filename | release.yml — the filename, not a path, not the workflow's name: |
| Environment | leave blank |
Three ways this is commonly filled in wrong:
- "Pending publisher" is the form for projects that do not exist yet. It
will never match
barber-llm, which is already on PyPI. Use the project's own publishing settings page. - Environment must be empty.
release.ymldeclares noenvironment:, so the OIDC token carries no environment claim, and any value typed in that box can never match. - Workflow filename is
release.yml. Not.github/workflows/release.yml, and notrelease(thename:at the top of the file).
If the publish job later fails at "Prove PyPI accepts this workflow's identity", that step prints the OIDC claims PyPI matched against, field by field. Compare them line by line with the form above; "no corresponding publisher" on its own tells you nothing about which field disagrees.
This is the failure mode most likely to bite. release.yml's publish job asks
"is there anything to do?" first, and a version already on PyPI is treated
as a no-op, not an error — correctly, since the upload cannot be repeated. But
that step returns green and skips everything after it, including the auth
step. So:
A
releaserun against an already-published version exits green without ever contacting PyPI's OIDC endpoint. It says nothing at all about whether the trusted publisher is registered.
The only run that tests the publisher is one on a version genuinely not yet on PyPI. Re-running the workflow on an old tag, or dispatching it manually on a tree whose version is already up, proves nothing. Do not read those green checkmarks as "publishing is configured".
Corollary: the first real test of Trusted Publishing will be the first release after it is registered. Cut that one with time to fall back to the manual path.
Every release so far has gone up this way, and every release will until the trusted publisher exists.
A manual upload bypasses release.yml entirely, and with it the guard that
contrib/ never enters the wheel. That guard exists because
contrib/claude_code_hook.py is experimental and is not covered by the
published retention benchmark, so it must stay reachable in the source tree and
absent from every installed environment. The invariant rests on
packages = ["barber"] in pyproject.toml, one edit away from silently not
holding, and a PyPI upload cannot be undone. Run the check yourself, locally,
before uploading.
# 0. versions agree, tests pass, fixtures fresh
python tests/test_versions.py
pytest tests/ -q
python tests/gen_golden.py --check
# 1. clean build (a stale dist/ is how the wrong version gets uploaded)
rm -rf dist/ build/
python -m build
twine check dist/*
# 2. the guard release.yml would have run — contrib must not be in the WHEEL
python - <<'PY'
import glob, sys, zipfile
wheel = glob.glob("dist/*.whl")[0]
names = zipfile.ZipFile(wheel).namelist()
bad = [n for n in names if "contrib" in n]
if bad:
sys.exit(f"contrib leaked into the wheel: {bad}")
print(f"ok: {wheel} has {len(names)} entries, no contrib")
PY
# 3. confirm the artifacts carry the version you think they do
ls dist/
# 4. upload
twine upload dist/*The sdist does contain contrib/, deliberately — tests/test_hook.py
needs the hook it tests, and the wheel is governed separately by
packages = ["barber"]. The check above is wheel-only on purpose; do not
"fix" it to cover the sdist.
The repo-root .env is gitignored and holds the publish tokens. Never commit
it, never paste its contents anywhere, never echo it into a terminal that is
being recorded or shared, and never add a token to a workflow file or a repo
secret while Trusted Publishing is the intended end state — the whole point of
registering it is that no token needs to exist. Registering the trusted
publisher retires this section.
npm has no equivalent of the PyPI workflow; it is manual by design and unrelated to the Trusted Publishing question above.
python tests/test_versions.py # js/package.json vs js/index.js VERSION
python tests/gen_golden.py --check
cd js
node --test
npm publish --dry-run # check the file list against "files" in package.json
npm publishnpm publish --dry-run is worth the extra step: package.json's files array
is the only thing keeping the test fixtures and this repo's Python side out of
the tarball.
- Confirm the published artifacts report the right version:
pip install --no-cache-dir barber-llm==X.Y.Z && python -c "import barber; print(barber.__version__)"andnpx barber-llm@X.Y.Z --version. This is the check that would have caught 0.4.0 from the outside. - The npm and PyPI badges in
README.mdwill show different numbers. That is expected and documented injs/README.md; leave them alone.