From 65a39dd65cdaaab2441178e1fa9786def1a43303 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 27 Jul 2026 11:46:09 -0500 Subject: [PATCH] =?UTF-8?q?fix(release):=20unblock=20tagging=20=E2=80=94?= =?UTF-8?q?=20drop=20the=20self-defeating=20README=20slug=20rewrite,=20com?= =?UTF-8?q?pare=20versions=20not=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects, either of which fails a tag push on its own. Runbook F.1 ("validate the release under the new OIDC binding") could not be performed until both were fixed. 1. THE README SLUG REWRITE FAILED UNCONDITIONALLY. publish.ps1 used to rewrite the private source slug to the public mirror's before `build` embedded README.md as the PyPI long_description. That rewrite had been applied to this workflow itself, so at the cutover both sides of the substitution collapsed to the same string: the sed replaced the public slug with itself (a no-op) and the guard after it failed if that slug appeared in the README -- which it does, 19 times. Every tag push died there. That is not hypothetical: the v0.3.0 tag (2026-07-13) failed at this exact step, which is why the repo has ZERO published releases. Removed outright -- there is one repo now and README.md already names it, so there is nothing to rewrite. Same root cause as the release job's inverted `if:` guard fixed in #7: mirror-era logic the slug rewrite mangled and the cutover left behind. 2. TAG-VS-BUILT COMPARED AS STRINGS, IN BOTH JOBS. The trigger only fires on `vX.Y.Z` / `vX.Y.Z-*`, so a pre-release tag must be spelled with a hyphen -- while hatchling and PyPI normalise `0.3.0-rc1` to `0.3.0rc1`. A raw compare therefore forced __version__ to carry the non-canonical "0.3.0-rc1" purely to satisfy the gate, and the tag, the module attribute and the wheel filename could not all be canonical at once. In the HARNESS job it was worse than awkward: `built` is parsed out of the already-normalised wheel FILENAME, so the compare could NEVER match a pre-release tag -- and with PUBLISH_HARNESS=true that job runs AFTER the engine has uploaded, so it would fail half-published. Both now compare packaging.Version objects; a genuine mismatch still fails loudly. Verified by running the real comparison: 0.3.0rc1 vs tag v0.3.0-rc1 matches; 0.3.0-rc1 matches; 0.3.0 vs v0.3.0-rc1 does not; 0.3.1 vs v0.3.0 does not. Tests pin both, mutation-verified: restoring the slug step fails, and restoring the string compare fails. The existing "version==tag comparison" canary tracked the raw shell test by its exact spelling, so it is retargeted at the CHECK rather than its wording. STILL OWNER-ONLY for F.1, unchanged by this commit: * a `messagefoundry-harness` PyPI Trusted Publisher (PUBLISH_HARNESS is already true, so the harness job WILL attempt an upload on the next tag); * setting __version__ to the rc being cut -- canonical `0.3.0rc1` now works, tagged `v0.3.0-rc1`; * the tag itself, which publishes to PRODUCTION PyPI (there is no TestPyPI route). --- .github/workflows/release.yml | 76 +++++++++++++++++++++++++++------- tests/test_release_pipeline.py | 43 ++++++++++++++++++- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3847b90..5c37d9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,20 +71,20 @@ jobs: with: python-version: "3.14" - - name: Rewrite README repo slug for the public PyPI long_description - run: | - # PyPI renders README.md (pyproject `readme = "README.md"`) as the project description and - # fetches it anonymously. The source README points at the PRIVATE source repo - # (MEFORORG/MessageFoundry) so the team's GitHub view shows the private repo's own CI - # badges — but anonymous fetches of a private repo's Actions badge SVG 404 (broken badge - # images), and every body link 404s on click. The public mirror (MEFORORG/MessageFoundry) - # runs the same workflows and is anonymously reachable, so rewrite the slug here (the same - # rewrite the retired scripts/publish/publish.ps1 used to apply) before `python -m build` embeds - # the README. Ephemeral runner edit only — never committed. - sed -i 's#MEFORORG/MessageFoundry#MEFORORG/MessageFoundry#g' README.md - if grep -q 'MEFORORG/MessageFoundry' README.md; then - echo '::error::README slug rewrite left private-repo links'; exit 1 - fi + # REMOVED at the MEFORORG cutover: "Rewrite README repo slug for the public PyPI + # long_description". It rewrote the private source slug to the public mirror's before `build` + # embedded README.md, because PyPI fetches the long_description anonymously and a private repo's + # badge SVGs and body links 404 for anonymous readers. + # + # There is one repo now, and README.md already names it — so there is nothing to rewrite. Worse, + # the step could no longer even fail safely: publish.ps1's slug rewrite had been applied to this + # workflow itself, collapsing BOTH sides of the substitution to the same string. The sed became a + # no-op that replaced the public slug with itself, and the guard immediately after it failed if + # that slug appeared in the README — which it always does, 19 times. So the step failed on EVERY + # tag push. That is exactly what happened: the v0.3.0 tag (2026-07-13) died here, and the repo has + # zero published releases as a result. Same root cause as the release job's inverted `if:` guard — + # mirror-era logic that the slug rewrite mangled and the cutover left behind. + # (tests/test_release_pipeline.py rejects both the step and any self-substituting sed returning.) - name: Build sdist + wheel run: | @@ -118,9 +118,32 @@ jobs: built=$(/tmp/relsmoke/bin/python -c "import messagefoundry; print(messagefoundry.__version__)") echo "built version: $built" # On a tag push, the built version MUST equal the tag (single-source check). + # + # Compared as PEP 440 VERSIONS, not as strings. A pre-release tag has to be spelled with a + # hyphen to match the `v[0-9]+.[0-9]+.[0-9]+-*` trigger above (`v0.3.0rc1` fires nothing), so + # a string compare forced __version__ to carry the non-canonical "0.3.0-rc1" purely to satisfy + # the gate — while hatchling/PyPI normalise it to 0.3.0rc1 everywhere else, meaning the tag, + # the module attribute and the wheel filename could not all be canonical at once. Version() + # normalises both sides, so canonical "0.3.0rc1" in __init__.py matches tag v0.3.0-rc1 and the + # check still fails loudly on a genuine mismatch. if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then want="${GITHUB_REF_NAME#v}" - [ "$built" = "$want" ] || { echo "::error::built version $built != tag $want"; exit 1; } + /tmp/relsmoke/bin/pip install --quiet packaging + /tmp/relsmoke/bin/python - "$built" "$want" <<'PYVER' + import sys + from packaging.version import InvalidVersion, Version + + built, want = sys.argv[1], sys.argv[2] + try: + b, w = Version(built), Version(want) + except InvalidVersion as exc: + raise SystemExit(f"::error::unparseable version ({exc}) — built={built!r} tag={want!r}") + if b != w: + raise SystemExit( + f"::error::built version {built} != tag {want} (normalised {b} != {w})" + ) + print(f"version matches tag: {b}") + PYVER # A released wheel MUST ship the PEP 561 marker so an external consumer's mypy sees the # typed surface. Enforced ONLY on a tag, so the workflow_dispatch dry-run is never blocked # before py.typed (WS-3) merges. This is the CI enforcement of the cross-workstream rule @@ -306,9 +329,30 @@ jobs: run: | built=$(python -c "import glob,re; print(re.search(r'messagefoundry_harness-([^-]+)-', glob.glob('harness-dist/*.whl')[0]).group(1))") echo "harness wheel version: $built" + # PEP 440 comparison, for a STRONGER reason than the engine's copy of this check: `built` is + # read out of the WHEEL FILENAME, which hatchling has already normalised. On tag v0.3.0-rc1 + # the file is messagefoundry_harness-0.3.0rc1-...whl, so built=0.3.0rc1 while want=0.3.0-rc1 + # and a string compare could NEVER match — this job failed on every pre-release tag by + # construction, whatever __version__ said. With PUBLISH_HARNESS=true it also runs after the + # engine has already uploaded, so the failure would land half-published. if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then want="${GITHUB_REF_NAME#v}" - [ "$built" = "$want" ] || { echo "::error::harness wheel version $built != tag $want"; exit 1; } + python -m pip install --quiet packaging + python - "$built" "$want" <<'PYVER' + import sys + from packaging.version import InvalidVersion, Version + + built, want = sys.argv[1], sys.argv[2] + try: + b, w = Version(built), Version(want) + except InvalidVersion as exc: + raise SystemExit(f"::error::unparseable version ({exc}) — built={built!r} tag={want!r}") + if b != w: + raise SystemExit( + f"::error::harness wheel version {built} != tag {want} (normalised {b} != {w})" + ) + print(f"harness version matches tag: {b}") + PYVER fi - name: Attach the harness wheel to the GitHub release diff --git a/tests/test_release_pipeline.py b/tests/test_release_pipeline.py index 5e5e922..7a36684 100644 --- a/tests/test_release_pipeline.py +++ b/tests/test_release_pipeline.py @@ -133,7 +133,10 @@ def test_release_load_bearing_canaries_present() -> None: "leak gate exits nonzero": "exit 1", # version single-sourced from the package == the tag "version==tag single-source": 'want="${GITHUB_REF_NAME#v}"', - "version==tag comparison": '[ "$built" = "$want" ]', + # The comparison itself. Was the raw string test `[ "$built" = "$want" ]`; that could not + # accept a canonical pre-release (0.3.0rc1 vs tag v0.3.0-rc1), so it is now a PEP 440 + # Version() compare. The canary tracks the CHECK existing, not how it is spelled. + "version==tag comparison": "from packaging.version import InvalidVersion, Version", # py.typed (WS-3) enforced on a tag push "py.typed enforced on tag": "unzip -l dist/*.whl | grep -q 'messagefoundry/py.typed'", "py.typed only on a tag": 'GITHUB_REF_TYPE:-}" = "tag"', @@ -265,3 +268,41 @@ def test_release_jobs_are_gated_ON_the_source_repo() -> None: ) # The private vault must never be a release target either. assert "wshallwshall" not in rel, "release.yml must not reference the retired private vault" + + +def test_no_self_referential_slug_rewrite_survives() -> None: + """The README slug rewrite is GONE, and no `sed s#X#X#` may come back. + + publish.ps1 rewrote the private slug to the public one across *.yml — including this workflow — + so at the cutover both sides of the substitution collapsed to the same string, leaving a no-op + sed followed by a guard that failed if that string was present. The README names it 19 times, so + the step failed on EVERY tag push: the v0.3.0 tag died there and the repo has no releases. + """ + rel = _release() + assert "- name: Rewrite README repo slug" not in rel, ( + "the mirror-era README slug rewrite is back — there is one repo now, so it rewrites nothing, " + "and its 'left private-repo links' guard then fails on every tag" + ) + assert not re.search(r"sed[^\n]*s([#/|])([^\n#/|]+)\1\2\1", rel), ( + "a self-referential sed (s#X#X#) is present — it cannot transform anything, and paired with " + "a grep guard it fails unconditionally" + ) + + +def test_both_wheel_smokes_compare_versions_not_strings() -> None: + """Tag-vs-built comparison must normalise (PEP 440), in BOTH the engine and harness jobs. + + The trigger only fires on `vX.Y.Z` / `vX.Y.Z-*`, so a pre-release tag must carry a hyphen, while + hatchling and PyPI normalise `0.3.0-rc1` to `0.3.0rc1`. A raw string compare therefore cannot be + satisfied by a canonical version — and in the HARNESS job it can never be satisfied at all, since + its `built` is parsed out of the already-normalised wheel FILENAME. + """ + rel = _release() + assert '[ "$built" = "$want" ]' not in rel, ( + "a raw string compare of tag vs built version is back; it rejects canonical pre-release " + "versions (0.3.0rc1 != 0.3.0-rc1) and blocks every rc tag" + ) + assert rel.count("from packaging.version import") == 2, ( + "both the engine and harness wheel smokes must compare PEP 440 versions — fixing only one " + "moves the failure rather than removing it" + )