Packaging + release CD: PyPI trusted publishing, GitHub Releases, version-coupled HF weights#133
Conversation
Uncomment and finalize the hatchling build backend (wheel target src/aetherscan) and resolve both stale TODO headers. Fill [project].dependencies from requirements-container.txt's ranges plus the packages the NGC base image provides implicitly (tensorflow[and-cuda]==2.17.*, h5py, hdf5plugin, psutil) — a pip install has no base image to lean on. dev extra gains pre-commit and pytest. Version single-sourcing: pyproject's version (kept a pre-release, 0.9.0.dev0 — the release PR bumps it) is now the only place the version is written; __init__.py reads it back via importlib.metadata with a 0.0.0.dev0 fallback so source-tree/container runs work uninstalled. The pre-existing CUDA '12 :: 12.8' classifier is not in the trove list (tops out at 12 :: 12.6) and fails hatchling validation — replaced with the valid ':: 12' parent. Development Status keeps a TODO knob for the v1 Beta-vs-Production call. gitignore gains python -m build artifacts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the inference revision-resolution chain with a version-pinned
default between --hf-revision and latest-release resolution: explicit
local paths > --hf-revision > v{__version__} > latest semver v* tag >
latest final_vX tag > error with guidance. version_default_revision()
makes 'pip install aetherscan==1.0.0' + bare inference pull exactly the
v1.0.0-blessed weights — the runtime half of docs/RELEASE.md's contract.
Guard: the default only activates when v{__version__} lands in the
release-tag family (strict vX.Y.Z via _SEMVER_TAG_PATTERN). The
importlib.metadata fallback (0.0.0.dev0), .dev pre-releases, and
rc/post/local versions fall through to latest-release resolution, so
source-tree and dev-install runs keep tracking the newest blessed
weights. Deliberately not existence-checked: an installed release whose
weights tag is missing fails the download loudly rather than silently
pulling another version's weights.
--hf-revision help text updated; README inference CLI block regenerated
(top/train verified unchanged). Tests cover the guard shapes, precedence,
pin-without-listing, dev fallthrough, and the end-to-end installed-release
download; existing no-revision tests now pin the dev version explicitly
so they never depend on whether aetherscan happens to be pip-installed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.github/workflows/release.yml on push of v* tags, gates ordered so a half-released state is impossible: 1. Version guard — tag must equal v + pyproject version, else fail loudly (the release PR's bump didn't land on the tagged commit). 2. Unit tests — tests.yml called as a reusable workflow (it gains a workflow_call trigger), so the release gate runs the exact CI suite by construction rather than by copy-paste. 3. HF weights verification — the matching v* tag must exist on the public model repo (no token). Verify, never create: the failure message points at utils/hf_tag_release.py and docs/RELEASE.md step 3. 4. Build — python -m build (sdist+wheel) plus a wheel smoke: --no-deps install must report exactly the guarded version. 5. PyPI via trusted publishing — pypa/gh-action-pypi-publish, OIDC (id-token: write) scoped to the pypi environment; no stored token. One-time maintainer setup documented in the workflow header. 6. GitHub Release — gh release create --verify-tag --generate-notes with the dist files attached; body curated from claude-release-notes comments afterwards per the runbook. Dry run: workflow_dispatch with test_pypi publishes to test.pypi.org under the testpypi environment; manual runs refuse to target real PyPI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release runbook's step 3 (docs/RELEASE.md): create the HF release
tag vX.Y.Z pointing at a training upload's commit —
python utils/hf_tag_release.py --save-tag final_v1 --release v1.0.0
A thin wrapper over HfApi.create_tag, standalone on purpose (no
PYTHONPATH or aetherscan install required). Validates the release-tag
shape, loads HF_TOKEN from the gitignored .env when python-dotenv is
available, and turns the common failure modes into targeted errors:
missing token, save-tag not on the repo (train with --hf-upload first),
and an already-existing release tag (immutable by convention). This is
the human blessing decision the release CD deliberately cannot make —
it only verifies the tag exists before publishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @zachtheyek's task in 2m 37s —— View job PR Review: Packaging + Release CD
OverallWell-structured PR. The version-coupling design — one string flows from Strengths
Issues and Suggestions1. 2. Consider pinning the 3. Missing 4. 5. 6. Test gap — Security
Minor Observations
Verdict: This is solid work. The architecture is well-thought-out, the code is clean, the tests are comprehensive, and the documentation (both in code and in the PR body) is excellent. The suggestions above are improvements, not blockers. |
|
|
| REF_NAME: ${{ github.ref_name }} | ||
| TEST_PYPI: ${{ inputs.test_pypi }} | ||
| run: | | ||
| VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") |
There was a problem hiding this comment.
Nit: open('pyproject.toml','rb') leaves the file handle to the garbage collector — fine in a short-lived CI step, but using a with statement would be more consistent with the rest of the codebase's style:
| VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") | |
| VERSION=$(python3 -c " | |
| import tomllib, pathlib | |
| print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())) | |
| " | head -1) |
Actually, the one-liner works because tomllib.load needs a binary stream — but a simpler cleanup would be:
VERSION=$(python3 -c "
import tomllib
with open('pyproject.toml', 'rb') as f:
print(tomllib.load(f)['project']['version'])
")Low priority — the step exits immediately anyway.
There was a problem hiding this comment.
Fixed in 57089c8 — the guard now reads pyproject.toml inside a with block (kept tomllib.load on the binary handle rather than the loads/read_text variant, per your second suggestion).
| RELEASE_TAG: v${{ needs.guard.outputs.version }} | ||
| run: | | ||
| # Range mirrors requirements-container.txt | ||
| pip install "huggingface_hub>=1.21,<1.22" |
There was a problem hiding this comment.
The huggingface_hub version installed here (>=1.21,<1.22) mirrors requirements-container.txt — good. Worth noting that this inline pip install doesn't benefit from setup-python's pip cache (no cache-dependency-path references this workflow for this job). Not a real problem since this step is fast, but if the install ever grows, a pinned requirements file or a cache key would help.
There was a problem hiding this comment.
Agreed it's negligible today: one pure-Python wheel installed once per release run. Left uncached deliberately — a cache key would add maintenance for no measurable win at this size. If this step ever grows a real dependency set, moving it to a pinned requirements file + setup-python cache is the right shape.
| tests: | ||
| needs: guard | ||
| uses: ./.github/workflows/tests.yml |
There was a problem hiding this comment.
Good design: tests and verify-weights both depend only on guard, so they run in parallel. The build job then fans-in on all three. This means a failing test suite doesn't block the fast HF tag check and vice versa — the failure surfaces as early as possible.
There was a problem hiding this comment.
Yes — that fan-out/fan-in is deliberate: the tag check is seconds while the suite is minutes, so a skipped blessing step surfaces almost immediately instead of hiding behind a full test run.
|
|
||
| api = HfApi() | ||
| try: | ||
| api.create_tag(args.repo_id, tag=args.release, revision=args.save_tag) |
There was a problem hiding this comment.
The revision=args.save_tag parameter is key — it tells create_tag to point the new release tag at the same commit as the training tag. This is the correct semantic for "blessing" weights. Clear and correct.
There was a problem hiding this comment.
Right — revision=<save_tag> resolves the training tag to its commit on the Hub side, so the release tag and the training tag end up pointing at the same upload. That's the whole blessing semantic.
| runbook's blessing step was skipped) rather than silently pull some other version's | ||
| weights. | ||
| """ | ||
| import aetherscan # noqa: PLC0415 (late import so tests can monkeypatch __version__) |
There was a problem hiding this comment.
The late import aetherscan to enable monkeypatch in tests is a pragmatic pattern. The alternative (injecting __version__ as a parameter) would ripple through the call chain. This is clean as-is — the noqa + comment make the intent clear.
There was a problem hiding this comment.
Exactly the tradeoff considered — threading __version__ through as a parameter would touch every caller of the resolution chain for a value that's conceptually ambient. The late import keeps the seam in one place.
| def test_dev_version_falls_through_to_latest_release(self, monkeypatch): | ||
| _set_version(monkeypatch, "0.9.0.dev0") | ||
| monkeypatch.setattr(hf_hub, "list_hf_tags", lambda repo_id: ["final_v1", "v0.2.0"]) | ||
| assert resolve_hf_revision("ns/repo", None) == "v0.2.0" |
There was a problem hiding this comment.
Good: existing tests that previously didn't pin __version__ now correctly pin it to "0.0.0.dev0" so they don't accidentally pass/fail depending on whether the package is installed in the test environment. This is the right fix for test hermeticity.
There was a problem hiding this comment.
Thanks — this bit — the tests must not care whether the suite happens to run in a venv with aetherscan pip-installed. Pinning __version__ per-test was the only way to make the no-revision paths hermetic.
…in setigen - release.yml version guard reads pyproject.toml via a with statement instead of leaving the handle to the GC (reviewer nit). - hf_tag_release.py rejects an empty/whitespace --save-tag up front with a clear message instead of surfacing it as a Hub 404 later. - setigen gains a >=2.6,<3 range (proven 2.6/2.7 line, 2.7.0 in current use) in pyproject.toml, requirements-container.txt, and environment.yml in lockstep — it was the only unpinned dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the review — all six numbered items addressed. Fixes landed in 57089c8; per-item disposition:
Re-verified after the fixes: |
|
Re the issue-link note: expected for a stacked PR — |
|
@claude please perform a second-pass code review of this PR. Re-review the current diff for any remaining correctness bugs, security issues, or significant quality problems, and post your findings. If the diff is clean, please say so explicitly. (Final verification round before merge.) |
Second-pass code review — final verificationI've re-reviewed the full diff (release.yml, tests.yml, pyproject.toml, environment.yml, requirements-container.txt, init.py, cli.py, hf_hub.py, hf_tag_release.py, test_hf_hub.py, .gitignore, README.md) for correctness bugs, security issues, and significant quality problems. The diff is clean. No remaining correctness bugs, security issues, or significant quality problems found. What I verifiedWorkflow (
Reusable workflow (
Packaging (
Version single-sourcing (
Version-coupled resolution (
Helper script (
Tests (
No security issues found. No secrets in code, no injection vectors, no over-permissioned workflows, no unsafe input handling. LGTM — ready to merge (after #130 lands and this rebases onto 🤖 Generated with Claude Code |
Closes #132
Implements packaging + release CD per the release contract in
docs/RELEASE.md(on #131's branch): one version string couples the git tag, the GitHub Release, the PyPI release, and the HF weights tag — with CD enforcing the coupling by verification, never by creating weights.What's here
1. Build system (
pyproject.toml)[build-system]uncommented, wheel targetsrc/aetherscan); both stale TODO headers resolved.[project].dependenciesfilled fromrequirements-container.txt's ranges plus the packages the NGC base image provides implicitly (tensorflow[and-cuda]==2.17.*,h5py>=3.11,<3.12,hdf5plugin,psutil>=6.0,<6.1— ranges mirrorenvironment.yml).requires-python = ">=3.10,<3.13"kept.devextra =ruff,pre-commit,pytest.2 - Pre-Alphawith a clearly-marked TODO knob for the v1 Beta-vs-Production decision (maintainer's call in the release PR).2. Version single-sourcing
version = "0.9.0.dev0"in pyproject is now the only place the version is written (pre-release on purpose; the release PR bumps it to1.0.0per the runbook).src/aetherscan/__init__.pyreads it back viaimportlib.metadata.version("aetherscan"), falling back to"0.0.0.dev0"onPackageNotFoundErrorso source-tree/container runs work uninstalled.3. Version-coupled weight resolution (
hf_hub.py)Resolution chain is now: explicit local paths →
--hf-revision→v{__version__}when running as an installed release → latest semverv*tag → latestfinal_vXtag → error with guidance. The new step (version_default_revision) is what makespip install aetherscan==1.0.0+ bare inference pull exactly thev1.0.0weights. Two deliberate design points, documented in the docstring:v{__version__}lands in the release-tag family (strictvX.Y.Z). The dev fallback (0.0.0.dev0),.devpre-releases, and rc/post/local versions all fall through to latest-release resolution.--hf-revisionhelp text updated accordingly → README inference CLI block regenerated withutils/print_cli_help.py(top/train blocks verified unchanged).4. CD workflow (
.github/workflows/release.yml) —on: push: tags: ["v*"], gates in order:v+ pyproject version, else fail loudly with pointer to the runbook.tests.ymlas a reusable workflow (it gained aworkflow_calltrigger), so the release gate runs the exact CI suite by construction instead of by copy-paste.zachtheyek/aetherscan(public read, no token). Verify-never-create; the failure message names the fix (utils/hf_tag_release.py) and says not to touch the git tag.python -m build(sdist + wheel), plus a wheel smoke —pip install --no-deps+__version__must equal the guarded version.pypa/gh-action-pypi-publish@release/v1withpermissions: id-token: writeand environmentpypi(trusted publishing; no stored token).gh release create --verify-tag --generate-noteswith the dist files attached (curate the body from theclaude-release-notescomments afterwards, per the runbook).Dry run:
workflow_dispatchwithtest_pypi: truepublishes to test.pypi.org under environmenttestpypi(recommended before the first real release). A manual run with the box unchecked fails the guard — real releases only ever go through tag pushes.5.
utils/hf_tag_release.py— the runbook's weight-blessing step:python utils/hf_tag_release.py --save-tag final_v1 --release v1.0.0creates the HF release tag pointing at the training upload's commit. Standalone (no PYTHONPATH/install needed), validates the tag shape, and gives targeted errors for missing token / missing save-tag / already-blessed release.Maintainer prerequisites (one-time, before the first release)
aetherscanand add GitHub as a trusted publisher: repositoryzachtheyek/Aetherscan, workflowrelease.yml, environmentpypi.pypienvironment in this repo's Settings → Environments.testpypienvironment.zachtheyek/aetherscanexists (created by HuggingFace Hub integration: opt-in weight upload after training, default weight download for inference, tag dedup guards #130's first--hf-upload).Deliberate divergences / notes vs
docs/RELEASE.mdtestpypienvironment + trusted publisher on test.pypi.org) aren't in RELEASE.md's one-time setup list — they're documented in the workflow header; RELEASE.md could gain a line when Documentation suite: one technical document per pipeline surface, indexed in docs/README.md #131 lands.Environment :: GPU :: NVIDIA CUDA :: 12 :: 12.8is not a valid trove classifier (the list tops out at12 :: 12.6) and brokepython -m buildthe moment validation turned on — replaced with the:: 12parent (the12.4entry is valid and kept).pip install aetherscanwith deps is Linux-only: TF 2.17'sand-cudaextra pinsnvidia-*wheels unconditionally (no platform markers upstream), and those wheels only exist for Linux. This matches the pipeline's supported runtimes (the clusters); it's why local verification below uses--no-deps.CITATION.cffstill saysversion: 0.1.0— it's a version-stamped artifact for the release PR to regenerate (runbook step 4, "regen anything version-stamped").Verification
python -m buildsucceeds (sdist + wheel); wheel installed into a fresh scratch venv withpip install --no-deps(see Linux-only note above), then:PYTHONPATH=src python -c ...→0.0.0.dev0(uninstalled).PYTHONPATH=src pytest -m "not gpu and not cluster" -q→ 530 passed, 3 skipped, 2 deselected (arm64 venv, TF 2.17.1; run after rebasing onto HuggingFace Hub integration: opt-in weight upload after training, default weight download for inference, tag dedup guards #130's current tip7cd19e1) — includes 11 new tests covering the full resolution chain: the dev-version guard (parametrized over dev/rc/post/local/partial versions), explicit-revision precedence, version-pin-without-listing, dev-fallthrough-to-latest, and the end-to-end installed-release download path.yaml.safe_loadclean onrelease.yml+tests.yml;actionlintnot available locally, so the workflow got a line-by-line manual review (documented gates, permissions per job, artifact handoff, environment-scoped OIDC).pre-commit run --all-filesgreen; ruff lint + format clean.AI disclosure
Implemented end-to-end with Claude Code (model: Claude Fable 5): packaging, resolution-chain extension, CD workflow, helper script, tests, and this PR body. Human review requested per AI_POLICY.md.
🤖 Generated with Claude Code