ci(release): unbreak PyPI publish — bump publish action past twine 6.1.0 - #63
Conversation
e0480fd to
9509e80
Compare
JosephSamirL
left a comment
There was a problem hiding this comment.
Review — convertcom/python-sdk PR #63 @ 9509e80
ci(release): unbreak PyPI publish — bump publish action past twine 6.1.0 · fix/release-metadata-2-5 → main · author abbaseya · 2 files, +2/−2 · reviewed 2026-09-17
VERDICT: CHANGES_REQUESTED (1 CRITICAL, 2 IMPORTANT)
The diagnosis is correct end to end — every causal claim about hatchling, twine and the failing run reproduces from source and from a scratchpad build. The fix as pushed does not unbreak publish. The new uses: sha a892a5a6… is the annotated tag object of v1.14.2, not the commit it points to (dc37677b…). pypa/gh-action-pypi-publish turns github.action_ref into a GHCR image tag, no image exists under a892a5a6…, and a public repository that pinned the identical line already failed with manifest unknown. Merging this reproduces today's shape (prepare ok → publish fails → release skipped) with a different error, and 2.1.0 stays unpublished. Separately, the PR body describes a diff (hatchling>=1.31,<2) that is not the one pushed (hatchling<2).
PR-body claims — verified / refuted
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Run 35232748196 failed at the publish step with '2.5' is not a valid metadata version |
VERIFIED | gh api …/runs/35232748196/jobs: Compute version + build → success, Publish to PyPI (OIDC) → failure (step 3 Run pypa/gh-action-pypi-publish@ba38be9e…), Tag + GitHub Release → skipped. Log: Checking dist/convert_python_sdk-2.1.0-py3-none-any.whl: ERROR InvalidDistribution: Invalid distribution metadata: '2.5' is not a valid metadata version |
| 2 | Tag job skipped, nothing half-released; no v2.1.0 tag/Release; PyPI on 2.0.0 |
VERIFIED | gh release list: v2.0.0 (Latest, 2026-07-24), v1.0.1, v1.0.0. Tags API: v2.0.0 → 1f4bd828. PyPI JSON: info.version = 2.0.0, 2.0.0 uploaded 2026-07-24T17:04:17 |
| 3 | Old pin ba38be9e = v1.14.1; requirements/runtime.txt there pins twine==6.1.0 |
VERIFIED | git rev-parse v1.14.1^{commit} = ba38be9e461d3875417946c167d0b5f3d385a247 (tag object 2834a314…). git show v1.14.1:requirements/runtime.txt: twine==6.1.0, packaging==25.0 |
| 4 | New pin a892a5a6 = v1.14.2, which pins twine==7.0.0 |
REFUTED as a commit pin (twine pin VERIFIED) | gh api repos/pypa/gh-action-pypi-publish/git/ref/tags/v1.14.2 → object.type: "tag", object.sha: a892a5a6…; git/tags/a892a5a6… → object.sha: dc37677b2e1c63e2034f94d8a5b11f265b73ba33 (type commit, tagger Sviatoslav Sydorenko 2026-07-29T16:57:12Z). git cat-file -t a892a5a6… = tag; git rev-parse v1.14.2^{commit} = dc37677b… = current release/v1 head. git/commits/a892a5a6… → 404; commits/a892a5a6… → 422 No commit found for SHA. git show v1.14.2:requirements/runtime.txt: twine==7.0.0, packaging==26.2 |
| 5 | hatchling 1.31.0 emits Metadata 2.4, 1.32.0 emits 2.5; 1.32.0 landed 2026-08-11; 1.32.1 (2026-09-16) yanked "Broken by change to types" | VERIFIED | backend/src/hatchling/metadata/spec.py: DEFAULT_METADATA_VERSION = "2.4" at hatchling-v1.31.0, "2.5" at hatchling-v1.32.0. PyPI JSON: 1.32.0 uploaded 2026-08-11; 1.32.1 uploaded 2026-09-16, yanked=True, reason Broken by change to types. Corroboration: 1.30.0 (2026-06-01) yanked with reason "the default core metadata version was increased to 2.5 and some major projects like twine do not yet support it"; 1.30.1 reverted to 2.4. Reproduced (scratchpad venvs, py3.13, python -m hatchling build on this repo at 9509e80): 1.31.0 → Metadata-Version: 2.4; 1.32.0 → Metadata-Version: 2.5 |
| 6 | twine 6.1.0 rejects 2.5, twine 7.0.0 accepts; old twine was the laggard | VERIFIED (and sharpened) | The rejecting code is packaging.metadata._VALID_METADATA_VERSIONS, but twine 6.1.0 twine/package.py monkeypatches that list to a frozen […, "2.0", …, "2.4"], so it rejects 2.5 regardless of packaging version — reproduced: twine 6.1.0 + packaging 26.2 → same ERROR; twine 6.2.0 + packaging 26.2 → same ERROR; twine 7.0.0 + packaging 26.2 → PASSED (wheel + sdist). twine 7.0.0 (2026-07-27) changelog: "Fix uploading packages with metadata version 2.5. The fix no longer allows metadata version 2.0 … (#1317)". No 6.x accepts 2.5. Twine was the laggard, and 7.0.0 is the first release that isn't |
| 7 | "Verified against the real package … Metadata-Version 2.5 / twine 7.0.0 PASSED / twine 6.1.0 ERROR" | VERIFIED | Reproduced exactly (table row 6). twine 6.1.0 + packaging 25.0 against the 1.31.0 (2.4) build → PASSED, matching the July v2.0.0 publish |
| 8 | Fix item 2: "Bound the build backend to hatchling>=1.31,<2" |
REFUTED by the diff | pyproject.toml at 9509e80: requires = ["hatchling<2"]. The commit message says >=1.31 was tried and dropped because it is unsatisfiable on 3.9 — verified: hatchling ≥1.28.0 has requires_python >=3.10 (PyPI JSON); uv pip install "hatchling>=1.31,<2" on Python 3.9.6 → No solution found … hatchling>=1.31.0,<=1.32.0 depends on Python>=3.10; hatchling<2 resolves to 1.27.0 on 3.9 and 1.32.0 on 3.13. The PR body is stale relative to the commit |
| 9 | The unbounded backend "is the part that recurs"; <2 addresses it |
REFUTED | See Finding 3. Both hatchling metadata bumps happened inside 1.x (1.30.0, 1.32.0); a <2 cap changes nothing for that class |
| 10 | Last real publish v2.0.0 on 2026-07-24; Release runs 2026-08-12 and 2026-09-15 green with publish skipped | VERIFIED | Release runs: 34982774276 (09-15) and 31601523118 (08-12): prepare success, publish skipped, release skipped. Also 30637880488 (07-31) had the same shape — omitted from the body, immaterial |
| 11 | Do not re-run the failed run — it replays that commit's workflow file | VERIFIED | GitHub docs (re-running workflows): "The workflow will also use the same GITHUB_SHA (commit SHA) and GITHUB_REF (git ref) of the original event that triggered the workflow run." The failing run's head_sha is e5b946f2 (main's head at the time, which carries the old pin) |
| 12 | Merge → CI → Release fires on workflow_run with the corrected workflow |
Mechanism VERIFIED; outcome REFUTED | release.yml: on.workflow_run.workflows: ['CI'], types: [completed], branches: [main]; ci.yml name: CI, on.push.branches: [main]. Docs: workflow_run "will only trigger a workflow run if the workflow file exists on the default branch" and runs with GITHUB_SHA = last commit on the default branch. The corrected file will run — and fail at the docker pull (Finding 1) |
| 13 | semantic-release computes from tag v2.0.0, the feat commits from #62 derive 2.1.0; the ci: commit is not releasable; version is stamped, not hard-coded |
VERIFIED | git merge-base --is-ancestor v2.0.0 main → yes. git log v2.0.0..main includes feat(features): type_casting on run_feature and run_features, feat(features): experience_keys on run_feature, run_features and diagnose_feature, two fix(features): …. release.config.mjs: tagFormat: 'v${version}', commit-analyzer preset: 'conventionalcommits' with no releaseRules → defaults in @semantic-release/commit-analyzer@13.0.1 lib/default-release-rules.js (feat → minor, fix → patch; ci absent → no release). pyproject.toml dynamic = ["version"] read from src/convert_sdk/version.py (0.0.0 placeholder), stamped by the prepare job before uv build; the failing run built convert_python_sdk-2.1.0-py3-none-any.whl |
| 14 | "Metadata 2.5 is valid and PyPI accepts it" | VERIFIED | hatchling 1.32.0's own wheel on PyPI carries Metadata-Version: 2.5 (PEP 658 .metadata file for hatchling-1.32.0-py3-none-any.whl); pypa v1.14.2 release notes: "update of Twine to v7 … will let them upload their sdists and wheels containing core packaging metadata v2.5 to (Test)PyPI" |
| 15 | (implicit) Inputs, auth mode and permissions unchanged across the bump | VERIFIED | git diff v1.14.1 v1.14.2 -- action.yml is empty; only requirements/runtime.{in,txt} changed. packages-dir is a canonical input. permissions: id-token: write, environment: pypi, no password input. release.yml at 9509e80 parses (PyYAML) |
Findings
1. [CRITICAL] The new pin is the annotated tag object, not the release commit — publish will fail again with manifest unknown (confidence: 97)
File: .github/workflows/release.yml:128 — uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2
Convention: ReleaseProcess wiki, "Third-party actions are pinned to a commit SHA" — a tag-object id is not a commit SHA. hooks/gate note: the old pin ba38be9e… was the peeled commit; this bump silently changed the object type.
Evidence, in order of the failure path
a892a5a6…is a git tag object (git cat-file -t→tag;git/tags/a892a5a6…→ target commitdc37677b2e1c63e2034f94d8a5b11f265b73ba33). GitHub'scommits/{sha}API rejects it with 422No commit found for SHA. Thecontents/tarball endpoints peel it, which is why the Actions service can still fetch the action source — the failure is downstream.action.ymlat v1.14.2 is a composite: theCreate Docker container actionstep runscreate-docker-action.pywithREF: ${{ github.action_ref || … }}.set_imagein that script returnsdocker://ghcr.io/pypa/gh-action-pypi-publish:{REF}; the only non-registry path (Dockerfile) is forREPO_ID == '178055147', the action's own repository. There is no fallback build.github.action_refis theuses:ref verbatim (actions/runnerActionRunner.cs:SetGitHubContext("action_ref", repoPathReferenceAction.Ref)). The failing run's own log shows this: it pulledghcr.io/pypa/gh-action-pypi-publish:ba38be9e461d3875417946c167d0b5f3d385a247— the literal old pin.- GHCR manifest probe (anonymous pull token):
:a892a5a61159132606e93a2fa6f4358831b04d26→ 404;:dc37677b2e1c63e2034f94d8a5b11f265b73ba33→ 200;:v1.14.2→ 200;:ba38be9e…→ 200 (digestsha256:cc949598…, identical to the digest in the failing run's log). The registry tag list contains commit shas and release names, never tag-object ids. - Ground truth from an unrelated public repository that pinned the identical line —
mmzen/se_harness,.github/workflows/publish-pypi.ymlline 115uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2, run https://github.com/mmzen/se_harness/actions/runs/31514559160 (2026-08-11), stepPublish exact distributions with Trusted Publishing → failure:The composite's earlier steps ran (source resolved); the docker step failed. Same action version, same code path, same sha./usr/bin/docker run --name ghcriopypaghactionpypipublisha892a5a6… … ghcr.io/pypa/gh-action-pypi-publish:a892a5a61159132606e93a2fa6f4358831b04d26 Unable to find image 'ghcr.io/pypa/gh-action-pypi-publish:a892a5a61159132606e93a2fa6f4358831b04d26' locally docker: Error response from daemon: manifest unknown
Consequence: after merge, prepare succeeds, publish-pypi fails at "Run Docker container", release is skipped, no tag, 2.1.0 still absent — exactly today's state with a different error. The PR body's "After merge — no manual retry needed" becomes false, and another PR cycle is required to publish.
Fix:
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2dc37677b… is v1.14.2^{commit}, the current release/v1 head, and the sha carried by ~7,100 public files (Dependabot output) versus 31 for the tag object. Resolve future pins with git rev-parse <tag>^{commit}, or dereference git/ref/tags/<tag> → .object.type == "tag" → git/tags/<sha> → .object.sha. Update the commit message and PR body to the new sha.
Would resolve it: the pinned sha returns 200 from https://ghcr.io/v2/pypa/gh-action-pypi-publish/manifests/<sha>; after merge, the publish log reads Status: Downloaded newer image for ghcr.io/pypa/gh-action-pypi-publish:dc37677b… followed by PASSED for wheel and sdist.
2. [IMPORTANT] The PR body describes a diff that was not pushed (confidence: 95)
File: PR body, "The fix" item 2 and the cause table; pyproject.toml:26.
The body says "Bound the build backend to hatchling>=1.31,<2". The diff is requires = ["hatchling<2"]. The commit message has the correct story — >=1.31 was tried and is unsatisfiable on the 3.9 matrix row — and that story checks out: hatchling moved to requires_python >=3.10 at 1.28.0 (2025-11-27, not 1.31 as the commit message says; the conclusion is unchanged), uv pip install "hatchling>=1.31,<2" on 3.9.6 fails to resolve, and hatchling<2 picks 1.27.0 (Metadata 2.4) on 3.9 and 1.32.0 (Metadata 2.5) on 3.13. The uv sync --group dev --python 3.9 cells build the project editable and would have failed under >=1.31.
The body is the record this review approves against and what future readers see first; it currently presents a lower bound that does not exist as part of a "measured" fix.
Fix: rewrite item 2 to hatchling<2 with the 3.9 rationale from the commit message; change "1.31+" to "1.28+" in the commit message.
3. [IMPORTANT] The recurrence claim is not delivered — hatchling<2 is inert against the observed class and nothing else guards it (confidence: 85)
Files: pyproject.toml:26; commit message ("the unbounded backend is what let the metadata version change under us silently, and that is the part that recurs"); absence of .github/dependabot.yml; ci.yml build job.
Evidence
- Every metadata-version bump hatchling has shipped happened inside 1.x: 1.30.0 (2026-06-01, yanked for exactly this breakage) and 1.32.0 (2026-08-11). A
<2cap present in July would not have changed the 2026-09-17 outcome; it excludes only a hypothetical 2.0. It is harmless (no interpreter resolves differently than before; CI is green) and conventional (flit's docs recommendflit_core >=3.2,<4; hatch's own docs recommend an unbounded["hatchling"]), but it is not a guard. - The coupling that broke is "backend's default metadata version ≤ uploader's accepted list". It is not testable by a build-system bound; it is testable by running the uploader's check, and it is kept from drifting by keeping the uploader current.
- There is no
.github/dependabot.yml(the js-yaml bump in0100280is a Dependabot security update, which needs no config). pypa published v1.14.2 on 2026-07-29, 13 days before hatchling 1.32.0 — agithub-actionsDependabot config would have proposed the bump inside that window, and Dependabot writes the peeled commit sha, so Finding 1 would not have occurred either. - CI's
buildjob runsuv buildon 3.13 and never runstwine check, so a backend/uploader mismatch is only ever observed at publish time onmain, after the merge that should have caught it. - Pinning to a known-good minor (
<1.33) is the wrong tool: the package ships as a pure-Python wheel, so consumers never invoke the backend and an upper cap only constrains sdist builders — and it still tests nothing.
Fix (either resolves the finding):
(a) Add .github/dependabot.yml with package-ecosystem: github-actions (weekly), and add to the CI build job a uv run --with twine==7.0.0 twine check dist/* step with a comment tying the twine version to the publish-action pin; or
(b) remove the recurrence claim from the commit message and PR body (keep <2 as a conventional cap, described as such) and file (a) as a follow-up.
Notes (not findings)
release.config.mjs's comment and the ReleaseProcess wiki sayperf:is a no-release type.@semantic-release/commit-analyzer@13.0.1default rules (lib/default-release-rules.js) makeperf → patch; only the release-notes types map hides it. Pre-existing and out of scope; noted for the wiki drift routine.- GHCR images
:dc37677b…and:v1.14.2have different digests (created 2026-07-29T17:05:51Z and 17:01:18Z — separate builds of the same release day). Consistent with pypa's release pipeline; not proof of byte-identical content (see "not checked").
What I checked
- PR metadata, body, head commit message; diff
main..pr63(merge basee5b946f2= currentmain). - Failing run 35232748196: run object, job/step conclusions,
--log-failed; prior Release runs 30637880488 / 31601523118 / 34982774276 job shapes. gh release list, tags API, PyPI JSON forconvert-python-sdk.pypa/gh-action-pypi-publish: blobless clone;git rev-parseof both tags and their^{commit};git cat-file -t;requirements/runtime.txtat v1.14.1 and v1.14.2;action.ymlandcreate-docker-action.pyat v1.14.2;git diff v1.14.1 v1.14.2 -- action.yml; v1.14.2 release notes; GitHubgit/ref,git/tags,git/commits,commits,contents,tarballand codeload behaviour for the tag-object sha.- GHCR: manifest HEAD for seven tags, tag list, OCI config
createdfor two images. actions/runnerActionRunner.csforgithub.action_refsemantics; GitHub docs forworkflow_rundefault-branch rule and re-runGITHUB_SHA/GITHUB_REFsemantics.- Public-repo ground truth: GitHub code search for both shas (31 vs 7,136 hits);
mmzen/se_harnessrun 31514559160 log and the workflow file at its head sha;AboveColin/petsseriesandOthmanAdi/hf-exitruns (publish step skipped in both — no signal). - hatchling: PyPI release dates,
requires_python,yankedandyanked_reasonfor 1.26–1.32.1;DEFAULT_METADATA_VERSIONinbackend/src/hatchling/metadata/spec.pyat 1.27.0, 1.30.0, 1.30.1, 1.31.0, 1.32.0. - twine/packaging: PyPI
requires_distfor twine 6.0.0–7.0.0 and packaging 24.0–26.3;_VALID_METADATA_VERSIONSin packaging 25.0/26.0/26.2; the monkeypatch in twine 6.1.0twine/package.py; twine 7.0.0 changelog and issue #1317. - Reproduction (scratchpad only,
UV_CACHE_DIRand all venvs under the scratchpad, Python 3.13.12 and 3.9.6 from the system, nothing installed globally, no background processes): built this repo at 9509e80 with hatchling 1.31.0 and 1.32.0; rantwine checkwith twine 6.1.0+packaging 25.0, 6.1.0+26.2, 6.2.0+26.2, 7.0.0+26.2; resolvedhatchling>=1.31,<2andhatchling<2under 3.9 and 3.13. - semantic-release:
release.config.mjs,package.jsonscripts,yarn.lockcommit-analyzer version and its default release rules;git log v2.0.0..main;src/convert_sdk/version.py;pyproject.tomldynamic version. release.ymlandci.ymlat 9509e80: triggers, guards, permissions, environment, YAML parse; sweep of every sha-pinneduses:in the repo and the git object type of each sha (astral-sh/setup-uv@d4b2f3b6…is a commit; only the new pypa pin is a tag object).- PR CI checks at 9509e80: all green (CodeQL, PR title, Ruff, mypy, 15-cell test matrix, bounds-check lower/upper, build).
What I did NOT check
- The contents of the GHCR image
:dc37677b…(did not pull it); its provenance rests on pypa's tag list, creation time and the source pin at that commit. - A live run of the corrected workflow — no workflow was executed and nothing was posted to GitHub.
- The PyPI Trusted Publisher configuration on pypi.org (not visible from the outside); the failing run reached the metadata check before any OIDC exchange, so it is not implicated.
- The B-G7 / AgDR section of the PR body and gate markers — outside this review's scope.
- Whether GitHub's Actions service would resolve a tag-object sha for a non-composite (JS/Docker) action; not needed — the pypa action's own image lookup fails first, as the se_harness run shows.
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 <tag>^{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) <noreply@anthropic.com>
9509e80 to
174933d
Compare
|
Thanks — all three accepted and fixed at 1 (CRITICAL) — correct, and it was my error. I resolved the pin with So the pushed fix would have failed at the docker pull before twine ran — today's shape with a different error, as you said. Repinned to 2 — correct. The body was written against the first push and I amended the commit without updating it. Rewritten. Your 1.28.0 correction also holds: PyPI 3 — correct, and the more useful finding. Both metadata bumps happened inside 1.x, so
Noted and not actioned, as out of scope: the One thing your review did not cover that I found while fixing the first push, in case it is useful: the py3.9 matrix cells do not run 3.9. In run 35232477588 the |
JosephSamirL
left a comment
There was a problem hiding this comment.
Approved via /convert:approve. An independent code review ran through /convert:review, and this issues the B-G4 human marker at 174933d.
What broke
The
Releaserun for the 2.1.0 merge (run 35232748196) failed at the publish step:Nothing was published and nothing is half-released: the tag job is gated on publish, so it was skipped. There is no
v2.1.0tag, no GitHub Release, and PyPI is still on 2.0.0.mainhas the code; the version simply does not exist yet.Cause
Measured, not inferred:
ba38be9e= v1.14.1requirements/runtime.txtat that tag pinstwine==6.1.0requires = ["hatchling"]had no bounduv buildresolves the newest backend at build timehatchling 1.32.0 landed 2026-08-11. The last real publish was v2.0.0 on 2026-07-24, before it existed.
Why it hid for five weeks: the
Releaseruns on 2026-08-12 and 2026-09-15 both reported success with the publish job skipped — semantic-release found no releasable commits. The pipeline was green twice while broken. This merge is the first actual publish since July.(
hatchling 1.32.1, 2026-09-16, is yanked — "Broken by change to types" — and is a red herring. 1.32.0 already emits 2.5.)The fix
dc37677b=v1.14.2^{commit}, which pinstwine==7.0.0. Metadata 2.5 is valid and PyPI accepts it — the old twine was the laggard, so this follows the ecosystem rather than holding the backend back. The peel matters: the annotated tag objecta892a5a6is not a commit, and the action turnsgithub.action_refinto a GHCR image tag verbatim, so that id fails withmanifest unknownbefore twine runs. Caught in review — see below.twine checkin CI'sbuildjob with the same twine the publish action ships. The coupling that broke is the backend's default metadata version against the uploader's accepted list, and it was observable only at publish time onmain— after the merge that should have caught it. This is the actual guard..github/dependabot.ymlforgithub-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.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.hatchling>=1.31was tried first and is wrong: hatchling requires Python >=3.10 from 1.28.0, and NFR22 mandates a 3.9 floor, so it madebuild-system.requiresunsatisfiable on the bottom matrix row. Underhatchling<2the backend resolves per interpreter — 3.9 takes 1.27.0 (metadata 2.4), 3.13 takes 1.32.0 (2.5) — and twine 7.0.0 accepts both. The release build runs on 3.13.Verified against the real package, not a synthetic one:
Review round 1 — CHANGES_REQUESTED, all three findings fixed
a892a5a6is the annotated tag object, not the release commit; no GHCR image exists under it, so publish fails withmanifest unknowndc37677b(v1.14.2^{commit}). Verified independently:commits/a892a5a6→ 422; GHCR manifest probe →a892a5a6404,dc37677b200,v1.14.2200, old pinba38be9e200hatchling>=1.31,<2, the diff washatchling<2; "1.31+" should be "1.28.0+"requires_python:>=3.8at 1.27.0,>=3.10from 1.28.0 (2025-11-27)<2is inert against the observed classtwine checkin CI + Dependabot config.<2is now described as a conventional cap, and the twine check named as the guardAfter merge — no manual retry needed
Do not re-run the failed run: a re-run replays that commit's workflow file, which still carries the v1.14.1 pin, so it would fail identically.
Merging this pushes
main→CIruns →Releasefires onworkflow_runwith the corrected workflow. semantic-release computes from the last tag (v2.0.0), and thefeatcommits from #62 are still untagged, so it derives 2.1.0 and publishes it. This PR's ownci:commit is not releasable on its own and does not need to be.🤖 Generated with Claude Code
Decision records (B-G7)
None of the records below are this PR's provenance. This is a two-line CI fix; its reasoning is in the commit message and the cause table above.
B-G7 resolves AgDRs by BMAD feature slug —
2026-04-06-convert-python-sdk, a folder spanning many workflows — rather than by the run that produced them, so it requires every record under that slug in any PR touching this repo. These belong to the per-call bucketing-attributes work already merged in #62:Tracked as F-35. Note
AgDR-0184wasAgDR-0173two days ago — the renumber the full-stem rule exists to survive, and why a bare number would not do here.