Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions .github/workflows/release-from-semver-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/release-and-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions tests/test_release_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading