gabriel / muse public
Closed #64 Enhancement
filed by gabriel human · 114 days ago

Derive the SemVer release tag from pyproject.toml automatically

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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.0rc15 release 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.
◎ Activity2
●
gabriel opened this issue 114 days ago
○
gabriel 2 days ago

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 existing deploy/prune_releases.py (same packaging dependency, 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.sh as a new step 8, right after the smoke test succeeds:

TAG=$(python3 "$SCRIPT_DIR/pep440_to_semver_tag.py" "$MUSE_VERSION")
muse -C "$MUSE_REPO" tag add "$TAG" --message "release $MUSE_VERSION"

Wrapped in an explicit if/else so 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 via set -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). Regenerated musehub's CLAUDE.md adapter via muse agent-config sync --force (no-op here since Claude's adapter is a minimal @.museagent.md include, 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.sh for a real release and did not retroactively tag the current 0.2.1rc12 build — 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 in test_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 -n confirms the script still parses.

Merged to dev → main in both musehub (the helper + script + docs) and muse (ticked the local planning doc's checkboxes), pushed to local/staging/production for both branches in both repos.

Leaving this open pending muse#63 and the real-release QA step.

○
gabriel 2 days ago

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.

✓
closed this issue 2 days ago