fix(packaging): resolve license-file path so sdists pass twine check - #51
Merged
Merged
Conversation
PEP 639 forbids parent-directory indicators in license-files. Twine rejected every sdist with `InvalidDistribution: '../../LICENSE' is invalid for 'license-file'`, blocking the release workflow. Replace the `../../LICENSE` path with a plain `LICENSE` entry in each package's pyproject.toml, and add a `LICENSE` symlink in each package directory pointing at the repo-root LICENSE so the file resolves at build time without duplicating its contents. Verified: all 14 wheels and 14 sdists built via `uv build --all-packages` now pass `twine check`.
5 tasks
antosubash
added a commit
that referenced
this pull request
Apr 22, 2026
* fix(packaging): replace LICENSE symlinks with real files so sdists extract 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). * ci(release): harden workflow — tag after publish, preflight checks, testpypi 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
The 0.0.1 release workflow failed at
twine checkfor every sdist with:PEP 639 disallows parent-directory indicators in
license-files. The wheels happened to pass (the license file is renamed inside*.dist-info/licenses/), but the sdist writes the literal path intoPKG-INFOasLicense-File: ../../LICENSE, which newer twine rejects.Changes
license-files = ["../../LICENSE"]withlicense-files = ["LICENSE"]across all 14 distributable packages (4framework/*, 10modules/*).LICENSEsymlink in each package directory pointing at the repo-rootLICENSE, so hatchling resolves the file at build time without duplicating its contents. Git stores the symlinks (mode120000), and the sdist/wheel materialize the file as a regular copy.Verification
All 14 wheels and 14 sdists now report
PASSED.Test plan
uv build --all-packagessucceeds for every packagetwine checkpasses on every wheel and sdistReviewer notes
ubuntu-latestin CI). Windows dev-machines needcore.symlinks=truein git config to materialize them — same caveat already applies to other aspects of this repo.pyproject.tomlbuild config beyond the singlelicense-filesline; hatchling's default license-file handling picks up the symlinked file.