diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 87839adba..b85f17707 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -145,3 +145,57 @@ Creating a new release 10. **Publish to PyPI** - Approve the ``Latest Release`` workflow under ``Actions`` to publish the package to PyPI. + +Creating a pre-release +---------------------- + +Use pre-releases to publish alpha, beta, or release candidate versions. These follow +`PEP 440 `_ pre-release format (e.g., ``1.10.0a1``, ``1.10.0b1``, ``1.10.0rc1``). + +1. **Bump to a pre-release version** + + .. code-block:: bash + + make pre-release version=1.10.0a1 # First alpha + make pre-release version=1.10.0a2 # Second alpha + make pre-release version=1.10.0b1 # First beta + make pre-release version=1.10.0rc1 # First release candidate + +2. **Commit and push** + + .. code-block:: bash + + git add -A && git commit -m "chore(release): bump to v1.10.0a1" + git push origin HEAD + +3. **Create a GitHub pre-release** + + .. code-block:: bash + + gh release create v1.10.0a1 --prerelease --title "v1.10.0a1" + +4. **PyPI behavior** + + PyPI automatically marks PEP 440 pre-release versions: + + - Users **won't** get pre-releases via ``pip install advanced-alchemy`` + - Users can opt-in via ``pip install --pre advanced-alchemy`` + - Or pin explicitly: ``pip install advanced-alchemy==1.10.0a1`` + +Graduating from pre-release to stable +++++++++++++++++++++++++++++++++++++++ + +From the last release candidate, bump the ``pre`` part to move past ``rc`` to ``stable``: + +.. code-block:: bash + + make release bump=pre # e.g. 1.10.0rc1 โ†’ 1.10.0 + +Or skip to the next stable version directly: + +.. code-block:: bash + + make release bump=patch # From any version โ†’ next patch + make release bump=minor # From any version โ†’ next minor + +Then follow the standard `Creating a new release`_ steps above. diff --git a/Makefile b/Makefile index 7d4609bf8..8e4dfa792 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,31 @@ release: ## Bump version and create re @uv lock --upgrade-package advanced-alchemy @echo "${OK} Release complete ๐ŸŽ‰" +.PHONY: pre-release +pre-release: ## Start a pre-release: make pre-release version=1.10.0a1 + @if [ -z "$(version)" ]; then \ + echo "${ERROR} Usage: make pre-release version=X.Y.ZaN"; \ + echo ""; \ + echo "Pre-release workflow:"; \ + echo " 1. Start alpha: make pre-release version=1.10.0a1"; \ + echo " 2. Next alpha: make pre-release version=1.10.0a2"; \ + echo " 3. Move to beta: make pre-release version=1.10.0b1"; \ + echo " 4. Move to rc: make pre-release version=1.10.0rc1"; \ + echo " 5. Final release: make release bump=pre (from rc) OR bump=patch/minor (from stable)"; \ + exit 1; \ + fi + @echo "${INFO} Preparing pre-release $(version)... ๐Ÿงช" + @make clean + @make build + @uv run bump-my-version bump --new-version $(version) pre + @uv lock --upgrade-package advanced-alchemy + @echo "${OK} Pre-release $(version) complete ๐Ÿงช" + @echo "" + @echo "${INFO} Next steps:" + @echo " 1. Push: git push origin HEAD" + @echo " 2. Create a GitHub pre-release: gh release create v$(version) --prerelease --title 'v$(version)'" + @echo " 3. This will publish to PyPI with pre-release tags" + # ============================================================================= # Cleaning and Maintenance # ============================================================================= diff --git a/docs/releases.rst b/docs/releases.rst index 6708db2c9..d0fbaae44 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -23,7 +23,8 @@ Pre-release Versions ++++++++++++++++++++ Before a new major release, we will make ``alpha``, ``beta``, and release candidate (``rc``) releases, numbered as -``..``. For example, ``2.0.0alpha1``, ``2.0.0beta1``, ``2.0.0rc1``. +``..`` following `PEP 440 `_. +For example, ``2.0.0a1``, ``2.0.0b1``, ``2.0.0rc1``. - ``alpha`` Early developer preview. Features may not be complete and breaking changes can occur. diff --git a/pyproject.toml b/pyproject.toml index 785929f24..5a5958584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,16 +180,30 @@ current_version = "1.9.0" ignore_missing_files = false ignore_missing_version = false message = "chore(release): bump to v{new_version}" -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +parse = """(?x) + (?P\\d+)\\.(?P\\d+)\\.(?P\\d+) + ((?P
a|b|rc)(?P\\d+))?
+"""
 regex = false
 replace = "{new_version}"
 search = "{current_version}"
-serialize = ["{major}.{minor}.{patch}"]
+serialize = [
+    "{major}.{minor}.{patch}{pre}{pre_n}",
+    "{major}.{minor}.{patch}",
+]
 sign_tags = false
 tag = false
 tag_message = "chore(release): v{new_version}"
 tag_name = "v{new_version}"
 
+[tool.bumpversion.parts.pre]
+optional_value = "stable"
+first_value = "stable"
+values = ["a", "b", "rc", "stable"]
+
+[tool.bumpversion.parts.pre_n]
+first_value = "1"
+
 [[tool.bumpversion.files]]
 filename = "pyproject.toml"
 replace = 'version = "{new_version}"'
diff --git a/tools/prepare_release.py b/tools/prepare_release.py
index 9fdbf85de..4b2393b64 100644
--- a/tools/prepare_release.py
+++ b/tools/prepare_release.py
@@ -218,6 +218,7 @@ async def get_release_info(self) -> ReleaseInfo:
         )
 
     async def create_draft_release(self, body: str, release_branch: str) -> str:
+        is_prerelease = bool(re.search(r"(a|b|rc)\d+$", self._new_release_version))
         res = await self._api_client.post(
             "/releases",
             json={
@@ -225,6 +226,7 @@ async def create_draft_release(self, body: str, release_branch: str) -> str:
                 "target_commitish": release_branch,
                 "name": self._new_release_tag,
                 "draft": True,
+                "prerelease": is_prerelease,
                 "body": body,
             },
         )
@@ -380,7 +382,7 @@ def update_pyproject_version(new_version: str) -> None:
     # can't use tomli-w / tomllib for this as is messes up the formatting
     pyproject = pathlib.Path("pyproject.toml")
     content = pyproject.read_text()
-    content = re.sub(r'(\nversion ?= ?")\d+\.\d+\.\d+("\s*\n)', rf"\g<1>{new_version}\g<2>", content)
+    content = re.sub(r'(\nversion ?= ?")\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?("\s*\n)', rf"\g<1>{new_version}\g<2>", content)
     pyproject.write_text(content)
 
 
@@ -414,7 +416,7 @@ def cli(
     if base is None:
         base = _get_latest_tag()
 
-    if not re.match(r"\d+\.\d+\.\d+", version):
+    if not re.match(r"\d+\.\d+\.\d+((a|b|rc)\d+)?$", version):
         click.secho(f"Invalid version: {version!r}")
         sys.exit(1)