From 174933df53b4f2dd257ed1774f9cb09ce0e83567 Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Thu, 17 Sep 2026 17:28:12 +0300 Subject: [PATCH] =?UTF-8?q?ci(release):=20unbreak=20PyPI=20publish=20?= =?UTF-8?q?=E2=80=94=20bump=20publish=20action=20past=20twine=206.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Release run for the 2.1.0 merge failed at the publish step: InvalidDistribution: Invalid distribution metadata: '2.5' is not a valid metadata version Cause, measured end to end: - The publish action was pinned to ba38be9e = v1.14.1, whose requirements/runtime.txt pins twine==6.1.0. - `requires = ["hatchling"]` carried no bound, so `uv build` resolves the newest backend at build time. hatchling 1.31.0 emits Metadata-Version 2.4; 1.32.0 (2026-08-11) emits 2.5. Bisected locally. - twine 6.1.0 rejects 2.5; twine 7.0.0 accepts it. No 6.x accepts it: 6.1.0 freezes the valid list in twine/package.py, so the packaging version does not matter. Verified against this package's own wheel. It went unnoticed for five weeks because the last real publish was v2.0.0 on 2026-07-24, before hatchling 1.32.0 existed. The Release runs on 2026-08-12 and 2026-09-15 both reported success with the publish job SKIPPED — no releasable commits — so the pipeline was green while broken. Changes: - Pin the publish action to v1.14.2, which pins twine==7.0.0. The sha is dc37677b, `v1.14.2^{commit}`. The peel matters: the annotated tag object a892a5a6 was pushed first and is NOT a commit. The action turns github.action_ref into a GHCR image tag verbatim, and no image exists under the tag-object id, so it fails with `manifest unknown` before twine runs. Resolve an annotated tag with `git rev-parse ^{commit}`. - Run `twine check` in CI's build job with the same twine the publish action ships. This coupling — the backend's default metadata version against the uploader's accepted list — is what actually broke, and it was observable only at publish time on main, after the merge that should have caught it. - Add .github/dependabot.yml for github-actions. v1.14.2 shipped 13 days before hatchling 1.32.0; a weekly bump lands the new uploader inside that window, and Dependabot writes the peeled sha. - Cap the build backend at `hatchling<2`. A conventional cap, not a guard: both metadata bumps (1.30.0, 1.32.0) happened inside 1.x, so it would not have prevented this. The twine check is the guard. `hatchling>=1.31` was tried first and is wrong — hatchling requires Python >=3.10 from 1.28.0, and NFR22 mandates a 3.9 floor, so it made build-system.requires unsatisfiable on the bottom matrix row. Under `hatchling<2` the backend resolves per interpreter: 3.9 takes 1.27.0 (metadata 2.4), 3.13 takes 1.32.0 (2.5). twine 7.0.0 accepts both. The release build runs on 3.13. Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 17 +++++++++++++++++ .github/workflows/ci.yml | 5 +++++ .github/workflows/release.yml | 2 +- pyproject.toml | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..618d0eb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 + +# GitHub Actions only. Python dependencies are governed by pyproject's own +# bounds and the bounds-check matrix, which Dependabot cannot reason about. +# +# pypa/gh-action-pypi-publish v1.14.2 shipped 13 days before hatchling 1.32.0 +# raised the default core-metadata version to 2.5. A weekly bump would have +# landed the new uploader inside that window. Dependabot also writes the +# peeled commit sha, not the annotated tag object. +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: ci diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ea7b4e..d1a4b04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,6 +175,11 @@ jobs: run: uv python install 3.13 - name: Build distributions run: uv build + # Keep this twine version equal to the one release.yml's pinned + # pypa/gh-action-pypi-publish ships, or publish can reject a build CI + # accepted. Both move together. + - name: twine check (same twine the publish action runs) + run: uv run --with twine==7.0.0 twine check dist/* - name: Upload distribution artifacts uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5d859a..1c2ae61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,7 +125,7 @@ jobs: # Trusted Publisher must be configured on pypi.org for this repo + # workflow + environment (one-time manual step — see RELEASE.md). # No password/token input: the action exchanges the OIDC token. - - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: packages-dir: dist diff --git a/pyproject.toml b/pyproject.toml index 664dc2d..bf8d5ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ Repository = "https://github.com/convertcom/python-sdk" Changelog = "https://github.com/convertcom/python-sdk/releases" [build-system] -requires = ["hatchling"] +requires = ["hatchling<2"] build-backend = "hatchling.build" # Single-source the version from src/convert_sdk/version.py (qs-01). The