fix(packaging): unbreak sdists + harden release workflow - #52
Merged
Conversation
…tract PR #51 added a `LICENSE -> ../../LICENSE` symlink to every distributable package so `license-files = ["LICENSE"]` would resolve at build time without duplicating content. `uv build` + `twine check` passed locally, but hatchling preserves symlinks verbatim when building sdists, and safe tar extractors (including uv's cache unpacker) refuse to unpack entries whose symlink target escapes the archive root — breaking `uv sync --all-packages` in CI: × Failed to build `simple-module-auth @ modules/auth` ├─▶ failed to unpack `...simple_module_auth-0.0.1/LICENSE` ╰─▶ symlink destination for ../../LICENSE is outside of the target directory Replace each symlink with a regular file copy of the repo-root LICENSE. ~15 KB of duplication for portable sdists — the right trade. Verified: all 14 sdists + 14 wheels build, pass `twine check`, and extract cleanly to a scratch directory (which reproduces the original CI failure).
…estpypi smoke The release workflow had a handful of failure modes that would surface the first time something actually went wrong: * Tag was pushed before `uv build` ran, so a failed build (e.g. the sdist symlink bug in #51) left an orphan `v<version>` tag to clean up. * `git commit || exit 0` silently skipped the tag-push step when there were no file changes (e.g. re-dispatching the same version), letting the workflow proceed to publish without a matching git tag. * `mv dist-py/${name}-* to-publish/ 2>/dev/null || true` swallowed empty globs, so a typo in the matrix would silently publish nothing. * `testpypi` target never smoke-tested the publish path — defeating the point of a dry-run target. * `uv tool install` in smoke ran immediately after publish-pypi, with no retry for PyPI index propagation delay. * No gate on the commit being released: any dispatch against main would proceed even if CI was red. Restructured the job graph to: preflight → build → publish-{pypi,npm} → finalize → smoke[-testpypi] * `preflight` validates the version string, fetches tags and fails fast if `v<version>` already exists, and polls `gh run list` for the latest pr.yml conclusion on the target SHA (pr.yml now also runs on `push: main` so main-HEAD commits are graded in their own right). * `build` bumps versions in the working tree only and uploads artifacts — no git side effects. * `publish-pypi` uses `shopt -s nullglob` + array-length guard, hard-failing if the matrix key matches zero files. * `publish-npm` now publishes from the uploaded `.tgz` artifact instead of re-checking-out the tag, and reads auth from an `NPM_TOKEN` env var fed by `secrets.NPM_TOKEN` via a manually written `.npmrc`. * `finalize` runs only on `inputs.target == 'pypi'` and only after every publish succeeded; it re-applies the bump, commits, tags, and pushes. A failed build or publish therefore never leaves origin in a bumped state. * `smoke` and new `smoke-testpypi` both wrap install/sync/npm-install in 12×15 s retry loops for registry propagation. `smoke-testpypi` points `UV_INDEX_URL` at TestPyPI with a PyPI extra-index for transitive deps and runs Python-only (npm isn't published on the testpypi target). Also aligned action versions with pr.yml (`setup-uv@v8.0.0`, `setup-node@v6`, `checkout@v6`) and pinned Python to 3.12 in every release job.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related release-path fixes on top of #51:
LICENSE -> ../../LICENSEsymlinks in every distributable solicense-files = [\"LICENSE\"]would resolve without duplicating content. Hatchling preserves those symlinks verbatim in sdists, and safe tar extractors (uv's cache unpacker included) refuse entries whose symlink target escapes the archive root — which broke `uv sync --all-packages` in CI with errors like `symlink destination for ../../LICENSE is outside of the target directory`. Replaced all 14 symlinks with real file copies.Packaging fix
LICENSEas a regular file (git `type-change 120000 → 100644`).uv build --all-packages→ 14 wheels + 14 sdists, all 28 passtwine check, and every sdist extracts to a scratch directory without the symlink-escape error (the exact CI failure mode).Release workflow changes
New job graph:
```
preflight → build → publish-{pypi,npm} → finalize → smoke[-testpypi]
```
preflight
build
publish-pypi
shopt -s nullglob+ array-length guard — hard-fails if the matrix key matches zero files instead of silently publishing nothing.publish-npm
finalize (new, only on `target == 'pypi'`)
smoke / smoke-testpypi
Other
Pre-flight for the next real release
Test plan
uv build --all-packageslocally: 14/14 wheels + 14/14 sdists.twine check dist/*: 28/28 PASSED.