Skip to content

Commit 586d220

Browse files
Add Nox task to trigger a release (#378)
* Add Nox task for triggering the release and function to release by a release type * Fix doc link tree problem --------- Co-authored-by: Ariel Schulz <43442541+ArBridgeman@users.noreply.github.com>
1 parent cb379dc commit 586d220

File tree

8 files changed

+281
-112
lines changed

8 files changed

+281
-112
lines changed

doc/changes/unreleased.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Unreleased
2+
3+
## ✨ Features
4+
5+
* [#378](https://github.com/exasol/python-toolbox/pull/378/files): Add Nox task to trigger a release

doc/developer_guide/developer_guide.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
../design
1111
plugins
1212
modules/modules
13-
../user_guide/how_to_release
13+
how_to_release
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.. include:: ../shared_content/how_release.rst

doc/shared_content/how_release.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
How to Release?
2+
===============
3+
4+
Creating a Release
5+
++++++++++++++++++
6+
7+
#. Prepare the project for a new release:
8+
9+
.. code-block:: shell
10+
11+
nox -s release:prepare -- --type {major,minor,patch}
12+
13+
#. Merge your **Pull Request** to the **default branch**
14+
15+
#. Trigger the release:
16+
17+
.. code-block:: shell
18+
19+
nox -s release:trigger
20+
21+
What to do if the release failed?
22+
+++++++++++++++++++++++++++++++++
23+
24+
The release failed during pre-release checks
25+
--------------------------------------------
26+
27+
#. Delete the local tag
28+
29+
.. code-block:: shell
30+
31+
git tag -d "<major>.<minor>.<patch>""
32+
33+
#. Delete the remote tag
34+
35+
.. code-block:: shell
36+
37+
git push --delete origin "<major>.<minor>.<patch>"
38+
39+
#. Fix the issue(s) which led to the failing checks
40+
#. Start the release process from the beginning
41+
42+
43+
One of the release steps failed (Partial Release)
44+
-------------------------------------------------
45+
#. Check the GitHub action/workflow to see which steps failed
46+
#. Finish or redo the failed release steps manually
47+
48+
.. note:: Example
49+
50+
**Scenario**: Publishing of the release on GitHub was successfully but during the PyPi release, the upload step was interrupted.
51+
52+
**Solution**: Manually push the package to PyPi

doc/user_guide/how_to_release.rst

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1 @@
1-
How to Release?
2-
===============
3-
4-
Creating a Release
5-
++++++++++++++++++
6-
7-
1. Set a variable named **TAG** with the appropriate version numbers:
8-
9-
.. code-block:: shell
10-
11-
TAG="<major>.<minor>.<patch>"
12-
13-
#. Prepare the project for a new release:
14-
15-
.. code-block:: shell
16-
17-
nox -s release:prepare -- "${TAG}"
18-
19-
#. Merge your **Pull Request** to the **default branch**
20-
#. Switch to the **default branch**:
21-
22-
.. code-block:: shell
23-
24-
git checkout $(git remote show origin | sed -n '/HEAD branch/s/.*: //p')
25-
26-
#. Update branch:
27-
28-
.. code-block:: shell
29-
30-
git pull
31-
32-
#. Create a new tag in your local repo:
33-
34-
.. code-block:: shell
35-
36-
git tag "${TAG}"
37-
38-
#. Push the repo to remote:
39-
40-
.. code-block:: shell
41-
42-
git push origin "${TAG}"
43-
44-
.. hint::
45-
46-
GitHub workflow **.github/workflows/cd.yml** reacts on this tag and starts the release process
47-
48-
What to do if the release failed?
49-
+++++++++++++++++++++++++++++++++
50-
51-
The release failed during pre-release checks
52-
--------------------------------------------
53-
54-
#. Delete the local tag
55-
56-
.. code-block:: shell
57-
58-
git tag -d "${TAG}"
59-
60-
#. Delete the remote tag
61-
62-
.. code-block:: shell
63-
64-
git push --delete origin "${TAG}"
65-
66-
#. Fix the issue(s) which lead to the failing checks
67-
#. Start the release process from the beginning
68-
69-
70-
One of the release steps failed (Partial Release)
71-
-------------------------------------------------
72-
#. Check the Github action/workflow to see which steps failed
73-
#. Finish or redo the failed release steps manually
74-
75-
.. note:: Example
76-
77-
**Scenario**: Publishing of the release on Github was successfully but during the PyPi release, the upload step got interrupted.
78-
79-
**Solution**: Manually push the package to PyPi
1+
.. include:: ../shared_content/how_release.rst

exasol/toolbox/nox/_release.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
from __future__ import annotations
22

33
import argparse
4+
import re
5+
import subprocess
46
from pathlib import Path
5-
from typing import (
6-
List,
7-
Tuple,
8-
)
97

108
import nox
119
from nox import Session
1210

13-
from exasol.toolbox import cli
1411
from exasol.toolbox.nox._shared import (
1512
Mode,
1613
_version,
1714
)
1815
from exasol.toolbox.nox.plugin import NoxTasks
1916
from exasol.toolbox.release import (
17+
ReleaseTypes,
2018
Version,
2119
extract_release_notes,
2220
new_changelog,
@@ -29,13 +27,16 @@
2927
def _create_parser() -> argparse.ArgumentParser:
3028
parser = argparse.ArgumentParser(
3129
prog="nox -s release:prepare",
32-
usage="nox -s release:prepare -- [-h] version",
30+
usage="nox -s release:prepare -- [-h] [-t | --type {major,minor,patch}]",
3331
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
3432
)
3533
parser.add_argument(
36-
"version",
37-
type=cli.version,
38-
help=("A version string of the following format:" '"NUMBER.NUMBER.NUMBER"'),
34+
"-t",
35+
"--type",
36+
type=ReleaseTypes,
37+
help="specifies which type of upgrade is to be performed",
38+
required=True,
39+
default=argparse.SUPPRESS,
3940
)
4041
parser.add_argument(
4142
"--no-add",
@@ -89,6 +90,42 @@ def _add_files_to_index(session: Session, files: list[Path]) -> None:
8990
session.run("git", "add", f"{file}")
9091

9192

93+
class ReleaseError(Exception):
94+
"""Error during trigger release"""
95+
96+
97+
def _trigger_release() -> Version:
98+
def run(*args: str):
99+
try:
100+
return subprocess.run(
101+
args, capture_output=True, text=True, check=True
102+
).stdout
103+
except subprocess.CalledProcessError as ex:
104+
raise ReleaseError(
105+
f"failed to execute command {ex.cmd}\n\n{ex.stderr}"
106+
) from ex
107+
108+
branches = run("git", "remote", "show", "origin")
109+
if not (result := re.search(r"HEAD branch: (\S+)", branches)):
110+
raise ReleaseError("default branch could not be found")
111+
default_branch = result.group(1)
112+
113+
run("git", "checkout", default_branch)
114+
run("git", "pull")
115+
116+
release_version = Version.from_poetry()
117+
print(f"release version: {release_version}")
118+
119+
if re.search(rf"{release_version}", run("git", "tag", "--list")):
120+
raise ReleaseError(f"tag {release_version} already exists")
121+
if re.search(rf"{release_version}", run("gh", "release", "list")):
122+
raise ReleaseError(f"release {release_version} already exists")
123+
124+
run("git", "tag", str(release_version))
125+
run("git", "push", "origin", str(release_version))
126+
return release_version
127+
128+
92129
@nox.session(name="release:prepare", python=False)
93130
def prepare_release(session: Session, python=False) -> None:
94131
"""
@@ -97,14 +134,7 @@ def prepare_release(session: Session, python=False) -> None:
97134
parser = _create_parser()
98135
args = parser.parse_args(session.posargs)
99136

100-
if not _is_valid_version(
101-
old=(old_version := Version.from_poetry()),
102-
new=(new_version := args.version),
103-
):
104-
session.error(
105-
f"Invalid version: the release version ({new_version}) "
106-
f"must be greater than or equal to the current version ({old_version})"
107-
)
137+
new_version = Version.upgrade_version_from_poetry(args.type)
108138

109139
if not args.no_branch and not args.no_add:
110140
session.run("git", "switch", "-c", f"release/prepare-{new_version}")
@@ -146,3 +176,9 @@ def prepare_release(session: Session, python=False) -> None:
146176
"--body",
147177
'""',
148178
)
179+
180+
181+
@nox.session(name="release:trigger", python=False)
182+
def trigger_release(session: Session) -> None:
183+
"""trigger an automatic project release"""
184+
print(f"new version: {_trigger_release()}")

exasol/toolbox/release/__init__.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import subprocess
44
from dataclasses import dataclass
55
from datetime import datetime
6-
from functools import total_ordering
6+
from enum import Enum
7+
from functools import (
8+
total_ordering,
9+
wraps,
10+
)
711
from inspect import cleandoc
812
from pathlib import Path
913
from shutil import which
@@ -18,6 +22,29 @@ def _index_or(container, index, default):
1822
return default
1923

2024

25+
class ReleaseTypes(Enum):
26+
Major = "major"
27+
Minor = "minor"
28+
Patch = "patch"
29+
30+
def __str__(self):
31+
return self.name.lower()
32+
33+
34+
def poetry_command(func):
35+
@wraps(func)
36+
def wrapper(*args, **kwargs):
37+
cmd = which("poetry")
38+
if not cmd:
39+
raise ToolboxError("Couldn't find poetry executable")
40+
try:
41+
return func(*args, **kwargs)
42+
except subprocess.CalledProcessError as ex:
43+
raise ToolboxError(f"Failed to execute: {ex.cmd}") from ex
44+
45+
return wrapper
46+
47+
2148
@total_ordering
2249
@dataclass(frozen=True)
2350
class Version:
@@ -62,20 +89,24 @@ def from_string(version):
6289
return Version(*version)
6390

6491
@staticmethod
92+
@poetry_command
6593
def from_poetry():
66-
poetry = which("poetry")
67-
if not poetry:
68-
raise ToolboxError("Couldn't find poetry executable")
69-
70-
try:
71-
result = subprocess.run(
72-
[poetry, "version", "--no-ansi", "--short"], capture_output=True
73-
)
74-
except subprocess.CalledProcessError as ex:
75-
raise ToolboxError() from ex
76-
version = result.stdout.decode().strip()
94+
output = subprocess.run(
95+
["poetry", "version", "--no-ansi", "--short"],
96+
capture_output=True,
97+
text=True,
98+
)
99+
return Version.from_string(output.stdout.strip())
77100

78-
return Version.from_string(version)
101+
@staticmethod
102+
@poetry_command
103+
def upgrade_version_from_poetry(t: ReleaseTypes):
104+
output = subprocess.run(
105+
["poetry", "version", str(t), "--dry-run", "--no-ansi", "--short"],
106+
capture_output=True,
107+
text=True,
108+
)
109+
return Version.from_string(output.stdout.strip())
79110

80111

81112
def extract_release_notes(file: str | Path) -> str:

0 commit comments

Comments
 (0)