Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
4 changes: 4 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Unreleased

## ✨ Features

* [#378](https://github.com/exasol/python-toolbox/pull/378/files): Add Nox task to trigger a release
2 changes: 1 addition & 1 deletion doc/developer_guide/developer_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
../design
plugins
modules/modules
../user_guide/how_to_release
how_to_release
1 change: 1 addition & 0 deletions doc/developer_guide/how_to_release.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. include:: ../shared_content/how_release.rst
52 changes: 52 additions & 0 deletions doc/shared_content/how_release.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
How to Release?
===============

Creating a Release
++++++++++++++++++

#. Prepare the project for a new release:

.. code-block:: shell
nox -s release:prepare -- [-h]
#. Merge your **Pull Request** to the **default branch**

#. Trigger the release:

.. code-block:: shell
nox -s release:trigger
What to do if the release failed?
+++++++++++++++++++++++++++++++++

The release failed during pre-release checks
--------------------------------------------

#. Delete the local tag

.. code-block:: shell
git tag -d "<major>.<minor>.<patch>""
#. Delete the remote tag
.. code-block:: shell
git push --delete origin "<major>.<minor>.<patch>"
#. Fix the issue(s) which lead to the failing checks
#. Start the release process from the beginning
One of the release steps failed (Partial Release)
-------------------------------------------------
#. Check the Github action/workflow to see which steps failed
#. Finish or redo the failed release steps manually
.. note:: Example
**Scenario**: Publishing of the release on Github was successfully but during the PyPi release, the upload step got interrupted.
**Solution**: Manually push the package to PyPi
80 changes: 1 addition & 79 deletions doc/user_guide/how_to_release.rst
Original file line number Diff line number Diff line change
@@ -1,79 +1 @@
How to Release?
===============

Creating a Release
++++++++++++++++++

1. Set a variable named **TAG** with the appropriate version numbers:

.. code-block:: shell

TAG="<major>.<minor>.<patch>"

#. Prepare the project for a new release:

.. code-block:: shell

nox -s release:prepare -- "${TAG}"

#. Merge your **Pull Request** to the **default branch**
#. Switch to the **default branch**:

.. code-block:: shell

git checkout $(git remote show origin | sed -n '/HEAD branch/s/.*: //p')

#. Update branch:

.. code-block:: shell

git pull

#. Create a new tag in your local repo:

.. code-block:: shell

git tag "${TAG}"

#. Push the repo to remote:

.. code-block:: shell

git push origin "${TAG}"

.. hint::

GitHub workflow **.github/workflows/cd.yml** reacts on this tag and starts the release process

What to do if the release failed?
+++++++++++++++++++++++++++++++++

The release failed during pre-release checks
--------------------------------------------

#. Delete the local tag

.. code-block:: shell

git tag -d "${TAG}"

#. Delete the remote tag

.. code-block:: shell

git push --delete origin "${TAG}"

#. Fix the issue(s) which lead to the failing checks
#. Start the release process from the beginning


One of the release steps failed (Partial Release)
-------------------------------------------------
#. Check the Github action/workflow to see which steps failed
#. Finish or redo the failed release steps manually

.. note:: Example

**Scenario**: Publishing of the release on Github was successfully but during the PyPi release, the upload step got interrupted.

**Solution**: Manually push the package to PyPi
.. include:: ../shared_content/how_release.rst
76 changes: 64 additions & 12 deletions exasol/toolbox/nox/_release.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path
from typing import (
List,
Expand All @@ -17,6 +20,7 @@
)
from exasol.toolbox.nox.plugin import NoxTasks
from exasol.toolbox.release import (
ReleaseTypes,
Version,
extract_release_notes,
new_changelog,
Expand All @@ -29,13 +33,17 @@
def _create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="nox -s release:prepare",
usage="nox -s release:prepare -- [-h] version",
usage="nox -s release:prepare -- [-h] [-t | --type {major,minor,patch}]",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"version",
type=cli.version,
help=("A version string of the following format:" '"NUMBER.NUMBER.NUMBER"'),
"-t",
"--type",
type=ReleaseTypes,
help="specifies which type of upgrade is to be performed",
required=True,
choices=[rt.value for rt in list(ReleaseTypes)],
default=argparse.SUPPRESS,
)
parser.add_argument(
"--no-add",
Expand Down Expand Up @@ -89,6 +97,51 @@ def _add_files_to_index(session: Session, files: list[Path]) -> None:
session.run("git", "add", f"{file}")


def _type_release(release_type: ReleaseTypes, old_version: Version) -> Version:
upgrade = {
ReleaseTypes.Major: Version(old_version.major + 1, 0, 0),
ReleaseTypes.Minor: Version(old_version.major, old_version.minor + 1, 0),
ReleaseTypes.Patch: Version(
old_version.major, old_version.minor, old_version.patch + 1
),
}
return upgrade[release_type]


class ReleaseError(Exception):
"""Error during trigger release"""


def _trigger_release() -> Version:
def run(*args: str):
try:
return subprocess.run(
args, capture_output=True, text=True, check=True
).stdout
except subprocess.CalledProcessError as ex:
raise ReleaseError(f"failed to execute command {args}") from ex

branches = run("git", "remote", "show", "origin")
if not (result := re.search(r"HEAD branch: (\S+)", branches)):
raise ReleaseError("default branch could not be found")
default_branch = result.group(1)

run("git", "checkout", default_branch)
run("git", "pull")

release_version = Version.from_poetry()
print(f"release version: {release_version}")

if re.search(rf"{release_version}", run("git", "tag", "--list")):
raise ReleaseError(f"tag {release_version} already exists")
if re.search(rf"{release_version}", run("gh", "release", "list")):
raise ReleaseError(f"release {release_version} already exists")

run("git", "tag", str(release_version))
run("git", "push", "origin", str(release_version))
return release_version


@nox.session(name="release:prepare", python=False)
def prepare_release(session: Session, python=False) -> None:
"""
Expand All @@ -97,14 +150,7 @@ def prepare_release(session: Session, python=False) -> None:
parser = _create_parser()
args = parser.parse_args(session.posargs)

if not _is_valid_version(
old=(old_version := Version.from_poetry()),
new=(new_version := args.version),
):
session.error(
f"Invalid version: the release version ({new_version}) "
f"must be greater than or equal to the current version ({old_version})"
)
new_version = Version.upgrade_version_from_poetry(args.type)

if not args.no_branch and not args.no_add:
session.run("git", "switch", "-c", f"release/prepare-{new_version}")
Expand Down Expand Up @@ -146,3 +192,9 @@ def prepare_release(session: Session, python=False) -> None:
"--body",
'""',
)


@nox.session(name="release:trigger", python=False)
def trigger_release(session: Session) -> None:
"""releases the project automatically"""
print(f"new version: {_trigger_release()}")
27 changes: 27 additions & 0 deletions exasol/toolbox/release/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import subprocess
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from functools import total_ordering
from inspect import cleandoc
from pathlib import Path
Expand All @@ -18,6 +19,15 @@ def _index_or(container, index, default):
return default


class ReleaseTypes(Enum):
Major = "major"
Minor = "minor"
Patch = "patch"

def __str__(self):
return self.name.lower()


@total_ordering
@dataclass(frozen=True)
class Version:
Expand Down Expand Up @@ -77,6 +87,23 @@ def from_poetry():

return Version.from_string(version)

@staticmethod
def upgrade_version_from_poetry(t: ReleaseTypes):
poetry = which("poetry")
if not poetry:
raise ToolboxError("Couldn't find poetry executable")

try:
result = subprocess.run(
[poetry, "version", str(t), "--dry-run", "--no-ansi", "--short"],
capture_output=True,
)
except subprocess.CalledProcessError as ex:
raise ToolboxError() from ex
version = result.stdout.decode().strip()

return Version.from_string(version)


def extract_release_notes(file: str | Path) -> str:
"""
Expand Down
Loading