Derive the SemVer release tag from pyproject.toml automatically
Derive the SemVer release tag from pyproject.toml automatically
Background
pyproject.toml's version field must stay in canonical PEP 440 form
(0.2.0rc15, no hyphen) because that's the literal string hatchling embeds
in the built sdist's filename — publish_muse_release.sh and
smoke_muse.sh both derive the tarball URL directly from it. A hyphenated
form (0.2.0-rc15) was tried during the rc15 release and broke the publish
script's tarball-filename lookup (hatchling normalizes it away regardless of
what's written in the file), so it was reverted back to 0.2.0rc15.
Separately, muse tag add enforces strict SemVer 2.0, which requires the
hyphen before a pre-release identifier (v0.2.0-rc15 is valid, v0.2.0rc15
is rejected with "Version tag must start with 'v' ... is not valid semver").
These two canonical forms are incompatible at the string level — there is no
single literal that satisfies both PEP 440's canonical form and SemVer 2.0's
required hyphen. pyproject.toml stays the single source of truth in PEP 440
form (see decision above); the SemVer tag needs to be derived from it, not
hand-typed as a second, independently-maintained version string.
Goal
Whenever a release is tagged with muse tag add, the SemVer-hyphenated tag
string is computed from pyproject.toml's PEP 440 version automatically —
no one hand-types v0.2.0-rc15 and risks a typo or drift from the actual
shipped version.
Suggested approach
A small helper (Python, using the already-available packaging library)
that takes a PEP 440 version string and emits the SemVer-compatible tag:
from packaging.version import Version
def pep440_to_semver_tag(version_str: str) -> str:
v = Version(version_str)
pre = f"-{v.pre[0]}{v.pre[1]}" if v.pre else ""
return f"v{v.major}.{v.minor}.{v.micro}{pre}"
# pep440_to_semver_tag("0.2.0rc15") -> "v0.2.0-rc15"
Wire this into the standard release flow (documented in
musehub/.muse/agent.md's "Standard release flow" section) as a step after
publish_muse_release.sh succeeds:
TAG=$(python3 -c "from packaging.version import Version; v=Version('$MUSE_VERSION'); pre=f'-{v.pre[0]}{v.pre[1]}' if v.pre else ''; print(f'v{v.major}.{v.minor}.{v.micro}{pre}')")
muse tag add "$TAG" --message "release $MUSE_VERSION"
Deliverables
- Helper function (or inline one-liner, whichever fits the existing script style better) added to the release flow, not hand-typed.
musehub/.muse/agent.md's "Standard release flow" section updated with the new tagging step.- Manually QA'd once against a real release before considering this done
(tag the current
0.2.0rc15release once MWP verb QA — muse#63 — is fully signed off, per gabriel's explicit sequencing: manual QA first, then tag).
Out of scope
- Changing
pyproject.toml's format — settled as PEP 440, no hyphen, per the incident above. - Retroactively tagging any release prior to this ticket.
Closing per gabriel's go-ahead, now that muse#63 (the gating ticket) is closed.
Deliverables 1-2 (helper + docs, see prior comment) are done and tested. Deliverable 3 ("manually QA'd once against a real release") has not happened yet — muse tag list is still empty, no release has been cut since this landed — and is deliberately not being forced now just to close this ticket. The next time deploy/publish_muse_release.sh is run for a real release, the tagging step will fire automatically as part of that normal flow (no separate manual trigger needed), which will serve as the live QA this deliverable asked for.
Closing as done — the mechanism is built, tested, and wired in; its first real-world exercise will happen naturally on the next release.
Deliverables 1-2 done; deliverable 3 remains correctly blocked on muse#63 (still open) — leaving this ticket open.
Deliverable 1 — helper, wired into the release flow (not hand-typed)
Added
musehub/deploy/pep440_to_semver_tag.py, following the exact pattern of the existingdeploy/prune_releases.py(samepackagingdependency, same small-helper-with-CLI-entry shape). Converts a PEP 440 version string to its SemVer tag equivalent, e.g."0.2.1rc12"→"v0.2.1-rc12".Wired into
publish_muse_release.shas a new step 8, right after the smoke test succeeds:Wrapped in an explicit
if/elseso a tagging failure (e.g. the tag already exists from a prior run) is a warning with a manual-retry command printed, not a fatal abort viaset -euo pipefail— the release is already live by that point, so the script shouldn't fail loudly over a cosmetic tagging hiccup.Deliverable 2 — docs updated
musehub/.museagent.md's "Standard release flow" section now documents the automatic tagging step and explains why the tag is derived rather than hand-typed (the PEP 440 vs. SemVer 2.0 hyphen incompatibility from this ticket's Background). Regeneratedmusehub'sCLAUDE.mdadapter viamuse agent-config sync --force(no-op here since Claude's adapter is a minimal@.museagent.mdinclude, but ran it anyway for consistency).Deliverable 3 — manual QA against a real release — still pending, correctly
muse#63 is still open as of this comment. Per your explicit sequencing ("manual QA first, then tag"), I did not run
publish_muse_release.shfor a real release and did not retroactively tag the current0.2.1rc12build — both remain deferred exactly as scoped. The next time a real release is published (after #63 signs off), the tagging step will fire automatically as part of the normal flow; no separate manual trigger is needed beyond running the publish script itself.Tests
tests/test_pep440_to_semver_tag.py(11) — unit tests for the helper (rc/a/b pre-release forms, final releases, the documented dev-segment-dropped limitation, invalid-version rejection) plus CLI subprocess tests.tests/test_publish_muse_release_tag.py(6) — source-level assertions matching this repo's existing convention intest_publish_muse_release_prune.py(validates the script's wiring without executing real AWS/muse calls). Confirmed genuine RED against the pre-fix script (5 of 6 failed) before GREEN with the fix.Targeted regression suite — both new files plus
test_publish_muse_release_prune.py(unaffected sibling),test_deployment.py,test_prune_releases.py— all pass (76 passed), zero regressions.bash -nconfirms the script still parses.Merged to
dev→mainin bothmusehub(the helper + script + docs) andmuse(ticked the local planning doc's checkboxes), pushed tolocal/staging/productionfor both branches in both repos.Leaving this open pending muse#63 and the real-release QA step.