Repository: RasaHQ/rasa
Pull request: Release script alpha
**Proposed changes**:
- Add support for `alpha` keyword in the release script [here](https://github.com/RasaHQ/rasa/blob/c4d5302e3e9d6d4f442d15b42f1ad585f0d90d88/scripts/release.py#L129-L150)
- Use strict PEP440 format e.g. `1.10.0a1`
- Use this new simple package I worked on: https://pypi.org/project/pep440-version-utils/ it makes it simpler to follow PEP440
- Breaking change to communicate: `patch` is renamed to `micro`
- When an alpha version is released, do not update the changelog.
- If on a feature branch (not master, not 1.10.x), alpha versions will not use a new branch

**Status (please check what you already did)**:
- [ ] updated the changelog (please check [changelog](https://github.com/RasaHQ/rasa/tree/master/changelog) for instructions)
- [x] reformat files using `black` (please check [Readme](https://github.com/RasaHQ/rasa#code-style) for instructions)


Problem statement:
Update release script to support pre-releases
**Description of Problem**:
If you want to tag a pre-release the release script does not accept, for example, the version number `1.10.0a1`. It just supports `1.10.0-alpha1`. Also the release script deletes all changelog files and update the `CHANGELOG.rst` file. This should not happen on a pre-release. We want to keep those updates for the next minor release.

**Overview of the Solution**:
Add support for pre-release versions, such as `1.10.0a1`.
Skip updating the `CHANGELOG.rst` file for pre-releases.

Hints from issue discussion:
@tabergma I'm taking a look at this one and I don't see what you mean by "it just supports 1.10.0-alpha1", because to me, it doesn't even support this:

https://github.com/RasaHQ/rasa/blob/c4d5302e3e9d6d4f442d15b42f1ad585f0d90d88/scripts/release.py#L33-L37
When you execute `make release` it ask you for the next version
```
? What is the version number you want to release ('major', 'minor', 'patch' or valid version number)?
```
If you type in `1.10.0-alpha1`, it will fail.

EDIT: If you type in `1.10.0a1`, it will fail. `1.10.0-alpha1` works.
ok so it doesn't work at all for alpha. I was confused by what you wrote in the desc, because you said that "It just supports 1.10.0-alpha1"
Sorry my bad. Yes, the version `1.10.0-alpha1` works, but `1.10.0a1` fails.
`1.10.0a1` is not following the [semver spec](https://semver.org/). But `1.10.0-a1` or `1.10.0-alpha1` would.
Yes. However, the idea was to nevertheless support it. We could maybe just internally map `1.10.0a1` to `1.10.0-a1` or `1.10.0-alpha1` and print a warning for the user. 
but why diverge from semver?
Edit: I drafted a PR, check it out :)
@tmbo mentioned we should support it
But we should definitely not just fail if a version number is not supported, but rather print a useful error message.
ok on that note we can definitely display what a valid version number looks like

Commit message: more meaningful message to pick a good version

Patch to review:
diff --git a/scripts/release.py b/scripts/release.py
index 466efaedefc3..aeafd815afec 100644
--- a/scripts/release.py
+++ b/scripts/release.py
@@ -7,6 +7,7 @@
 """
 import argparse
 import os
+import re
 import sys
 from pathlib import Path
 from subprocess import CalledProcessError, check_call, check_output
@@ -25,6 +26,8 @@
 
 RELEASE_BRANCH_PREFIX = "prepare-release-"
 
+ALPHA_VERSION_PATTERN = re.compile(r"^alpha([1-9]\d*)$")
+
 
 def create_argument_parser() -> argparse.ArgumentParser:
     """Parse all the command line arguments for the release script."""
@@ -33,7 +36,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
     parser.add_argument(
         "--next_version",
         type=str,
-        help="Either next version number or 'major', 'minor', 'patch'",
+        help="Either next version number or 'major', 'minor', 'patch', 'alpha'",
     )
 
     return parser
@@ -68,9 +71,6 @@ def write_version_file(version: Text) -> None:
 
 def write_version_to_pyproject(version: Text) -> None:
     """Dump a new version into the pyproject.toml."""
-
-    import toml
-
     pyproject_file = pyproject_file_path()
 
     try:
@@ -130,16 +130,15 @@ def ask_version() -> Text:
     """Allow the user to confirm the version number."""
 
     def is_valid_version_number(v: Text) -> bool:
-        # noinspection PyBroadException
-        try:
-            return v in {"major", "minor", "patch"} or Version.coerce(v) is not None
-        except Exception:
-            # "coerce" did fail, this is probably not a valid version number
-            return False
+        return v in {"major", "minor", "patch", "alpha"} or validate_version(v)
 
+    current_version = Version.coerce(get_current_version())
+    next_patch_version = str(current_version.next_patch())
+    next_alpha_version = str(next_alpha(current_version))
     version = questionary.text(
-        "What is the version number you want to release "
-        "('major', 'minor', 'patch' or valid version number)?",
+        f"What is the version number you want to release "
+        f"('major', 'minor', 'patch', 'alpha' or valid version number "
+        f"e.g. '{next_patch_version}' or '{next_alpha_version}')?",
         validate=is_valid_version_number,
     ).ask()
 
@@ -228,6 +227,44 @@ def ensure_clean_git() -> None:
         sys.exit(1)
 
 
+def validate_version(version: Text) -> bool:
+    """
+    Ensure that the version follows semver
+    and that the alpha follows the format `alpha1`, `alpha2`, etc...
+    """
+    if not semantic_version.validate(version):
+        return False
+
+    version_object = Version.coerce(version)
+    return not version_object.prerelease or (
+        len(version_object.prerelease) == 1
+        and validate_alpha_version_part(version_object.prerelease[0])
+    )
+
+
+def validate_alpha_version_part(alpha_version_part: Text) -> bool:
+    """
+    Validate that the alpha part in a version follows
+    the pattern specified in `ALPHA_VERSION_PATTERN`.
+    """
+    return ALPHA_VERSION_PATTERN.match(alpha_version_part) is not None
+
+
+def next_alpha(version: Version) -> Version:
+    """Bump the current version to the next alpha."""
+    alpha_number = 0
+    if version.prerelease:
+        alpha_number = int(ALPHA_VERSION_PATTERN.match(version.prerelease[0]).group(1))
+
+    return Version(
+        major=version.major,
+        minor=version.minor,
+        patch=version.patch,
+        prerelease=(f"alpha{alpha_number + 1}",),
+        partial=version.partial,
+    )
+
+
 def parse_next_version(version: Text) -> Text:
     """Find the next version as a proper semantic version string."""
     if version == "major":
@@ -236,7 +273,9 @@ def parse_next_version(version: Text) -> Text:
         return str(Version.coerce(get_current_version()).next_minor())
     elif version == "patch":
         return str(Version.coerce(get_current_version()).next_patch())
-    elif semantic_version.validate(version):
+    elif version == "alpha":
+        return str(next_alpha(Version.coerce(get_current_version())))
+    elif validate_version(version):
         return version
     else:
         raise Exception(f"Invalid version number '{cmdline_args.next_version}'.")
@@ -280,7 +319,9 @@ def main(args: argparse.Namespace) -> None:
     write_version_file(version)
     write_version_to_pyproject(version)
 
-    generate_changelog(version)
+    if not version.prerelease:
+        # never update changelog on an alpha version
+        generate_changelog(version)
     base = git_current_branch()
     branch = create_release_branch(version)
 


Code review comments:
diff_hunk: @@ -280,7 +319,9 @@ def main(args: argparse.Namespace) -> None:
     write_version_file(version)
     write_version_to_pyproject(version)
 
-    generate_changelog(version)
+    if not version.prerelease:
+        # never update changelog on an alpha version
+        generate_changelog(version)
     base = git_current_branch()
     branch = create_release_branch(version)
path: scripts/release.py
text: @user1:
I think for alpha releases we need to differentiate: if the current branch is neither master nor a release branch (matching `1.2.x` pattern) I think we should directly commit to the current branch. this seems to work better for the workflow where you e.g. break model compatibility on a feature branch and need to increase the version number.

@author:
OK gotcha

diff_hunk: @@ -25,6 +26,8 @@
 
 RELEASE_BRANCH_PREFIX = "prepare-release-"
 
+ALPHA_VERSION_PATTERN = re.compile(r"^alpha([1-9]\d*)$")
path: scripts/release.py
text: @user1:
I am not sure if this has changed recently, the most recent PEP I am aware of is https://www.python.org/dev/peps/pep-0440/#pre-releases which uses the `1.2.3a2` format instead of `alpha`.

If there is no newer PEP suggesting `alpha`, we should stick with `a`

@author:
ok, so that's why! sadly they diverged from strict semver 😢

diff_hunk: @@ -228,6 +227,44 @@ def ensure_clean_git() -> None:
         sys.exit(1)
 
 
+def validate_version(version: Text) -> bool:
+    """
+    Ensure that the version follows semver
+    and that the alpha follows the format `alpha1`, `alpha2`, etc...
+    """
+    if not semantic_version.validate(version):
+        return False
+
+    version_object = Version.coerce(version)
+    return not version_object.prerelease or (
+        len(version_object.prerelease) == 1
+        and validate_alpha_version_part(version_object.prerelease[0])
+    )
+
+
+def validate_alpha_version_part(alpha_version_part: Text) -> bool:
path: scripts/release.py
text: @user1:
optional: to simplifiy the above return, I'd rename this to `is_alpha_version` and as a parameter I'd use `version: Version`, moving the 
```
(
        len(version_object.prerelease) == 1
        and validate_alpha_version_part(version_object.prerelease[0])
    )
```
code part into this property.

diff_hunk: @@ -236,7 +273,9 @@ def parse_next_version(version: Text) -> Text:
         return str(Version.coerce(get_current_version()).next_minor())
     elif version == "patch":
         return str(Version.coerce(get_current_version()).next_patch())
-    elif semantic_version.validate(version):
+    elif version == "alpha":
path: scripts/release.py
text: @user1:
optional: since we might soon want to cut an `rc`, can we easily make this more generic to also support these versions?

@author:
good, so we would add support for `X.YrcN` from PEP spec