From 9c79dec9b4ecfb76810263d36f419e9384dce1fa Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Tue, 5 May 2026 12:12:03 +0200 Subject: [PATCH 1/2] fix(packaging): bundle LICENSE in wheel and sdist The 2026.5.5.1 release upload to PyPI failed with ``400 License-File LICENSE does not exist in distribution file mergify_cli-2026.5.5.1.tar.gz at mergify_cli-2026.5.5.1/LICENSE``. Maturin auto-derives a ``License-File: LICENSE`` PEP 639 metadata field from ``license = "Apache-2.0"`` plus the project root ``LICENSE`` file, but the sdist tarball (``maturin sdist``) packs the cargo workspace and the Python source dir without picking up the project root ``LICENSE`` itself. PyPI's upload validator cross-checks the metadata against the tarball contents and 400s when they disagree. Listing ``LICENSE`` in ``[tool.maturin].include`` puts it back in both the wheel and the sdist, so the metadata claim is honored and PyPI accepts the upload. Co-Authored-By: Claude Opus 4.7 (1M context) Change-Id: Ie45717750af52467929600966592cb8453d3b4d2 --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4807f654..f96ce860 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,13 @@ manifest-path = "crates/mergify-cli/Cargo.toml" python-source = "." module-name = "mergify_cli" strip = true +# Bundle LICENSE in both the wheel and sdist. PEP 639 metadata +# (auto-derived by maturin from `license = "Apache-2.0"`) emits a +# `License-File: LICENSE` field, and PyPI's upload validator rejects +# the artifact with `400 License-File LICENSE does not exist in +# distribution file` if the file isn't actually bundled. Listing it +# here keeps wheel + sdist in sync with the metadata. +include = ["LICENSE"] [tool.pytest.ini_options] asyncio_mode = "auto" From 7f84caee351e177a184c2c3c2ccb19deb12e710f Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Tue, 5 May 2026 12:19:14 +0200 Subject: [PATCH 2/2] ci(packaging): validate sdist+wheel metadata with twine on every PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, packaging-metadata bugs only surface at ``release: published`` time — release 2026.5.5.1 was rejected by PyPI's upload validator with ``400 License-File LICENSE does not exist in distribution file`` because the LICENSE auto-bundling hadn't been wired up. PR CI couldn't have caught it: ``build-sdist`` was gated on ``inputs.stamp-version``, so PR runs skipped the sdist entirely, and there was no twine invocation anywhere. Two changes: 1. ``build-sdist`` now runs on every PR. The version-stamping step and the artifact upload still gate on ``inputs.stamp-version`` (PR builds keep the placeholder version and skip the upload — the artifact is only useful for the publish job in ``release.yml``). The Python toolchain is provisioned the same way as the wheel jobs. 2. ``twine check --strict`` runs against both the wheel (per matrix target) and the sdist immediately after each is built. Strict mode applies the same metadata rules PyPI's upload validator does — README rendering, ``Description-Content-Type``, ``License-File`` presence — so a mismatch fails PR CI instead of the next release. The cost is one extra ubuntu-24.04 job (~30s for sdist + twine check) and a few seconds per wheel-matrix shard for the twine install + check. Co-Authored-By: Claude Opus 4.7 (1M context) Change-Id: I0f7338df02bbf600804a58bcb996bcb882de6010 --- .github/workflows/build-wheels.yml | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 81301649..40f5adb8 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -88,6 +88,18 @@ jobs: manylinux: auto sccache: true + # `twine check --strict` validates the wheel's metadata against + # the same rules PyPI's upload validator applies (README + # rendering, License-File presence, Description-Content-Type, + # etc.). Running it on every PR catches packaging-metadata + # regressions at PR time instead of at the next `release: + # published` event. + - name: Validate wheel metadata + shell: bash + run: | + python -m pip install --quiet twine + twine check --strict target/wheels/*.whl + # Only upload artifacts when the caller actually needs them # (release.yml downloads them in its publish job). PR smoke # builds just need the build to succeed — keeping the @@ -103,16 +115,18 @@ jobs: name: build sdist timeout-minutes: 10 runs-on: ubuntu-24.04 - # The sdist is only useful for the release publish step. PR - # smoke runs would just throw it away — skip the whole job. - if: inputs.stamp-version steps: - uses: actions/checkout@v6.0.2 with: fetch-depth: 0 fetch-tags: true + - uses: actions/setup-python@v6.2.0 + with: + python-version: 3.14 + - name: Stamp wheel version from git tag + if: inputs.stamp-version shell: bash run: | set -eu @@ -125,7 +139,18 @@ jobs: command: sdist args: --out target/wheels + # See `Validate wheel metadata` above — twine's strict check + # exercises the metadata rules PyPI applies. Running it on PR + # would have caught the missing `License-File LICENSE` + # bundling that broke release 2026.5.5.1. + - name: Validate sdist metadata + shell: bash + run: | + python -m pip install --quiet twine + twine check --strict target/wheels/*.tar.gz + - uses: actions/upload-artifact@v7 + if: inputs.stamp-version with: name: wheel-sdist path: target/wheels/*.tar.gz