Skip to content

Commit a7db3cf

Browse files
committed
ci: Reconcile PEP 440 prerelease versions with semantic-release
semantic-release computes semver versions, but uv normalizes prerelease versions to PEP 440 when cutting them, so the tag it produces (v3.0.0b1) is not valid semver. semantic-release silently skips tags it cannot parse, so every run on a prerelease branch reported the channel's first prerelease as the next version and tried to cut a version that had already been published. Mirror each PEP 440 prerelease tag onto its semver equivalent before running semantic-release. The mirrored tags are local to the workflow run, so the PEP 440 tags stay the only real ones and continue to match the published package version. Recording the channel a prerelease went out on, which semantic-release requires before it will treat a prerelease tag as released, stays in the Version workflow where the release is cut. It now keys the note to the PEP 440 tag that actually gets created, rather than to a semver tag name that never exists, and derives the channel from the normalized version so a release cut by hand is recorded the same way. Notes attach to commits rather than tags, so the mirrored tag picks the channel up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKRv7ht6XA9Sm8brKctEva
1 parent 623287b commit a7db3cf

2 files changed

Lines changed: 42 additions & 9 deletions

File tree

.github/workflows/semantic-release.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ jobs:
2525
uses: actions/checkout@v7
2626
with:
2727
fetch-depth: 0
28+
- name: Mirror prerelease tags as semver
29+
run: |
30+
# Prerelease tags use PEP 440 (v3.0.0b1) to match the published
31+
# package version, but semantic-release only understands semver and
32+
# silently ignores tags it cannot parse. Without this, every run
33+
# reports the first prerelease of the channel as the next version.
34+
# Mirror each PEP 440 prerelease tag onto its semver equivalent
35+
# (v3.0.0-beta.1). These are local to the run and are never pushed;
36+
# the PEP 440 tags remain the only real ones. The channel notes the
37+
# Version workflow records against the tagged commit apply to the
38+
# mirrored tag too, since notes attach to commits, not tags.
39+
for tag in $(git tag --list 'v*'); do
40+
semver="$(
41+
printf '%s' "${tag#v}" | sed -E \
42+
-e 's/^([0-9]+\.[0-9]+\.[0-9]+)a([0-9]+)$/\1-alpha.\2/' \
43+
-e 's/^([0-9]+\.[0-9]+\.[0-9]+)b([0-9]+)$/\1-beta.\2/' \
44+
-e 's/^([0-9]+\.[0-9]+\.[0-9]+)rc([0-9]+)$/\1-rc.\2/'
45+
)"
46+
if [ "$semver" = "${tag#v}" ]; then
47+
continue
48+
fi
49+
50+
git tag --force "v$semver" "$tag^{commit}"
51+
echo "Mirrored $tag as v$semver."
52+
done
2853
- name: Semantic release
2954
id: release
3055
uses: cycjimmy/semantic-release-action@v6

.github/workflows/version.yml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,28 @@ jobs:
3131
passphrase: ${{ secrets.GPG_PASSPHRASE }}
3232
- name: Setup
3333
uses: ./.github/actions/setup
34+
# uv normalizes the semver version semantic-release computes
35+
# (3.0.0-beta.1) to PEP 440 (3.0.0b1), which is what gets committed,
36+
# tagged and published. The Semantic Release workflow mirrors that tag
37+
# back to semver so it can pick up where the last prerelease left off.
3438
- name: Cut ${{ github.event.inputs.version }} version
3539
run: |
3640
uv version "${{ github.event.inputs.version }}"
3741
just version
42+
# semantic-release only treats a prerelease tag as released when a note
43+
# records the channel it went out on, so record it here, once the release
44+
# exists. The note is keyed to the commit rather than the tag, which is
45+
# what lets the mirrored semver tag pick it up.
3846
- name: Record prerelease channel
39-
env:
40-
VERSION: ${{ github.event.inputs.version }}
4147
run: |
42-
case "$VERSION" in
43-
*-*) channel="${VERSION#*-}"; channel="${channel%%.*}" ;;
48+
tag="v$(uv version --short)"
49+
case "$(uv version --short)" in
50+
*.*.*a[0-9]*) channel=alpha ;;
51+
*.*.*b[0-9]*) channel=beta ;;
52+
*.*.*rc[0-9]*) channel=rc ;;
4453
*) echo "Stable release, no channel note required."; exit 0 ;;
4554
esac
46-
git fetch origin "+refs/notes/semantic-release:refs/notes/semantic-release" || true
47-
git notes --ref semantic-release add --force \
48-
--message "{\"channels\":[\"$channel\"]}" "v$VERSION^{commit}"
49-
git push origin refs/notes/semantic-release
50-
echo "Recorded v$VERSION on the '$channel' channel."
55+
git notes --ref "semantic-release-$tag" add --force \
56+
--message "{\"channels\":[\"$channel\"]}" "$tag^{commit}"
57+
git push origin "refs/notes/semantic-release-$tag"
58+
echo "Recorded $tag on the '$channel' channel."

0 commit comments

Comments
 (0)