diff --git a/.github/workflows/release-from-semver-label.yml b/.github/workflows/release-from-semver-label.yml index ea556a8..0700a14 100644 --- a/.github/workflows/release-from-semver-label.yml +++ b/.github/workflows/release-from-semver-label.yml @@ -83,6 +83,16 @@ jobs: output("version", version) output("tag", f"v{version}") + def next_version_from_label(version: str, label: str) -> str: + major, minor, patch, prerelease = parse_version(version) + if label == "semver:major": + return f"{major + 1}.0.0" + if label == "semver:minor": + return f"{major}.{minor + 1}.0" + if prerelease: + return f"{major}.{minor}.{patch}" + return f"{major}.{minor}.{patch + 1}" + message = os.environ["HEAD_COMMIT_MESSAGE"] if os.environ["GITHUB_ACTOR"] == "github-actions[bot]" and re.match(r"^Release v\d+\.\d+\.\d+(?:rc\d+)?$", message.strip()): print("Release bump commit detected; no additional release bump needed.") @@ -147,27 +157,16 @@ jobs: major, minor, patch, prerelease = parse_version(current_version) if prerelease: tag = f"v{current_version}" - if run("git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}", check=False).returncode == 0: - print(f"Release tag {tag} already exists.", file=sys.stderr) - raise SystemExit(1) - print(f"PR #{pr_number} declares prerelease version {current_version}; releasing it without another bump.") - set_release_outputs(current_version) - output("pr_number", pr_number) - output("semver_label", selected[0]) - raise SystemExit(0) + if run("git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}", check=False).returncode != 0: + print(f"PR #{pr_number} declares prerelease version {current_version}; releasing it without another bump.") + set_release_outputs(current_version) + output("pr_number", pr_number) + output("semver_label", selected[0]) + raise SystemExit(0) + print(f"Prerelease tag {tag} already exists; applying {selected[0]} to compute the next release.") label = selected[0] - if label == "semver:major": - major += 1 - minor = 0 - patch = 0 - elif label == "semver:minor": - minor += 1 - patch = 0 - else: - patch += 1 - - next_version = f"{major}.{minor}.{patch}" + next_version = next_version_from_label(current_version, label) tag = f"v{next_version}" if run("git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}", check=False).returncode == 0: print(f"Release tag {tag} already exists.", file=sys.stderr) diff --git a/docs/release-and-versioning.md b/docs/release-and-versioning.md index 4f8d223..e6673ac 100644 --- a/docs/release-and-versioning.md +++ b/docs/release-and-versioning.md @@ -22,7 +22,7 @@ Package-affecting PRs must have exactly one semver label: `semver:major`, `semve After a package-affecting PR merges to `master`, CI reads the merged PR label, bumps `project.version` in `pyproject.toml`, updates `uv.lock`, reruns tests and lint, rebuilds wheel/sdist artifacts, proves wheel installation from `dist/`, commits `Release vMAJOR.MINOR.PATCH`, pushes the matching tag, and attaches the artifacts to the GitHub Release. -If a merged package-affecting PR explicitly sets `project.version` to a prerelease `MAJOR.MINOR.PATCHrcN` version, CI releases that exact version and marks the GitHub Release as a prerelease instead of computing an additional semver bump. +If a merged package-affecting PR explicitly sets `project.version` to a prerelease `MAJOR.MINOR.PATCHrcN` version, CI releases that exact version and marks the GitHub Release as a prerelease instead of computing an additional semver bump when the matching prerelease tag does not exist. If the prerelease tag already exists, the release resolver applies the merged PR's semver label instead; `semver:patch` promotes the prerelease base to the stable `MAJOR.MINOR.PATCH` version. Direct package-affecting pushes to `master` do not have PR labels to inspect. They may still publish a release when the push explicitly changes `project.version` in `pyproject.toml` to an unreleased `MAJOR.MINOR.PATCH` version. In that path CI uses the explicit version, refreshes `uv.lock`, proves the artifact, tags the existing commit, and publishes the GitHub Release. diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py index ca6a695..2ae91d4 100644 --- a/tests/test_release_workflows.py +++ b/tests/test_release_workflows.py @@ -81,6 +81,9 @@ def test_master_release_workflow_bumps_from_merged_pr_label() -> None: assert "MAJOR.MINOR.PATCHrcN" in workflow assert "declares prerelease version" in workflow assert "releasing it without another bump" in workflow + assert "def next_version_from_label" in workflow + assert "Prerelease tag {tag} already exists; applying" in workflow + assert 'return f"{major}.{minor}.{patch}"' in workflow assert "Package-affecting direct push did not change pyproject.toml" in workflow assert 'output("release_needed", "false")' in workflow assert "set_release_outputs(current_version)" in workflow