Skip to content
Open
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
41 changes: 41 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Auto Version Bump

on:
pull_request:
types: [labeled]

jobs:
bump-version:
if: >
github.event.label.name == 'patch' ||
github.event.label.name == 'minor' ||
github.event.label.name == 'major' ||
github.event.label.name == 'release'
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Run version bump script
run: |
python tools/bump_version.py ${{ github.event.label.name }}

- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
commit-message: "chore: bump version (${{ github.event.label.name }})"
title: "chore: bump version (${{ github.event.label.name }})"
body: |
This version bump was triggered by PR #${{ github.event.pull_request.number }}
Label applied: **${{ github.event.label.name }}**
labels: version-bump
base: main
branch: auto-version-bump/${{ github.event.pull_request.number }}
81 changes: 81 additions & 0 deletions tools/bump_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import sys

# The GitHub label passed as an argument: patch, minor, major, release
LABEL = sys.argv[1]

# The file that stores the current version
VERSION_FILE = "version.txt"

def read_version():
"""Read the current version from version.txt"""
with open(VERSION_FILE, "r") as f:
return f.read().strip()

def write_version(version):
"""Write the new version back to version.txt"""
with open(VERSION_FILE, "w") as f:
f.write(version + "\n")

def parse_version(v):
"""
Parse a version string like "0.5.0-draft" into parts:
major, minor, patch, and optional suffix (e.g., "-draft").
"""
parts = v.split(".") # Split into ['0', '5', '0-draft']
major = int(parts[0])
minor = int(parts[1])

# Handle patch number and suffix(if exist)
if "-" in parts[2]:
patch_str, suffix = parts[2].split("-", 1)
patch = int(patch_str)
suffix = "-" + suffix # Keep the dash in the suffix
else:
patch = int(parts[2])
suffix = ""

return major, minor, patch, suffix


def bump_version(major, minor, patch):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe instead we could have just 3 new variables, and we should probably throw an exception for unknown LABELs

new_patch = 0
new_minor = 0
new_major = 0

if LABEL == "patch":
    new_patch = patch + 1
elif LABEL == "minor":
    new_minor = minor + 1
elif LABEL == "major":
    new_major = major + 1
else
    raise ExceptionType(f"invalid LABEL: {LABEL}")

return new_major, new_minor, new_patch

Additionally, we may need a suffix or prefix potentially, e.g. "v1.0.0-beta". But I think that can come when we need it. It looks like you started some functionality for suffix, but I dont think it's done.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks you for the feedback!

I understand the suggestion to use three new variables, it does make the bump logic more explicit and clear. I also see your point about throwing an exception for unknown labels. In the current setup, the GitHub workflow only passes predefined labels (patch, minor, major, release), so in practice an exception isn’t strictly necessary.

That said, adding an exception could serve as a useful safeguard if someone were to run the script manually with an invalid label. I’ll update the script to incorporate both of these suggestions.

"""
Increment the version based on LABEL:
- patch: increment patch
- minor: increment minor, reset patch
- major: increment major, reset minor and patch
"""
if LABEL == "patch":
patch = patch + 1
elif LABEL == "minor":
minor = minor + 1
patch = 0
elif LABEL == "major":
major = major + 1
minor = 0
patch = 0
return major, minor, patch

def main():
# Read current version
old_version = read_version()

# Parse current version
major, minor, patch, suffix = parse_version(old_version)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to suffix?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the question! The suffix is parsed so that I can correctly separate the numeric patch value from any trailing suffix such as -draft. I designed the logic under the assumption that versions should always include -draft unless the label is release, in which case the suffix is intentionally removed.

So the suffix is extracted purely so the script can reliably parse versions like 0.5.0-draft without errors, and so a release label can strip the suffix. Outside of that case, the script intentionally replaces the suffix with -draft on all non-release bumps.


# If label is release, remove the "-draft" suffix
if LABEL == "release":
new_version = f"{major}.{minor}.{patch}"
else:
# Otherwise, bump version according to label
new_major, new_minor, new_patch = bump_version(major, minor, patch)
new_version = f"{new_major}.{new_minor}.{new_patch}-draft"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use suffix here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, the original assumption was that only finalized releases should omit the -draft suffix. That’s why I intentionally append -draft during version bumps, so all non-release versions are clearly marked.

If the goal is to preserve existing suffixes (potentially for more complex cases in the future like -beta) the logic can certainly be updated to consistently use the parsed suffix instead of forcing -draft. The current implementation mainly ensures that non-release versions are easily distinguishable from official releases.


# Prevent race conditions by only writing if version changed
if new_version != old_version:
write_version(new_version)
print(f"Version updated: {old_version} -> {new_version}")
else:
print(f"No version change needed. Current version: {old_version}")

if __name__ == "__main__":
main()