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

## Summary

## Features

* #740: Added nox session `release:update`
44 changes: 30 additions & 14 deletions doc/user_guide/features/creating_a_release.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Creating a release
Creating a Release
==================

Preparing a release
Preparing a Release
+++++++++++++++++++

#. Prepare the project for a new release
Expand All @@ -10,15 +10,18 @@ Preparing a release

nox -s release:prepare -- --type {major,minor,patch}

The ``release:prepare`` nox session affects the ``pyproject.toml``, ``version.py``, and files in the ``doc/changes`` directory:
The ``release:prepare`` nox session affects the ``pyproject.toml``,
``version.py``, and files in the ``doc/changes`` directory:

* Creates & switches to a release branch (can be skipped with ``--no-branch``)
* Updates the version in the ``pyproject.toml`` and ``version.py``
* Moves the content of unreleased changes file ``unreleased.md`` to a versioned changes file ``changes_<version>.md``
* Moves the content of unreleased changes file ``unreleased.md`` to a
versioned changes file ``changes_<version>.md``
* Adds a description of dependency changes to the versioned changes file:

* Only direct dependencies are described, no transitive dependencies
* Changes are detected by comparing the current content of file ``poetry.lock`` to the latest Git tag.
* Changes are detected by comparing the current content of file
``poetry.lock`` to the latest Git tag.
* Updates the ``changelog.md`` list with the newly created versioned changes file
* Commits the changes (can be skipped with ``--no-add``)
* Pushes the changes and creates a PR (can be skipped with ``--no-pr``)
Expand All @@ -34,22 +37,34 @@ Preparing a release
Use the nox session ``release:trigger`` to:

* Switch to & pull the changes from the default branch
* Verify that the version to be released does not already have a git tag or GitHub release
* Create a new tag & push it to the default branch, which will trigger the GitHub workflow ``cd.yml``
* Verify that the version to be released does not already have a git tag
or GitHub release
* Create a new tag & push it to the default branch, which will trigger the
GitHub workflow ``cd.yml``

Additionally, if enabled in your project config, the task will create an additional tag with pattern ``v<MAJOR_VERSION>``.
This is especially useful if other projects use Github actions of your project, for example:
Additionally, if enabled in your project config, the task will create an
additional tag with pattern ``v<MAJOR_VERSION>``. This is especially
useful if other projects use Github actions of your project, for example:

.. code-block:: yaml

uses: exasol/your_project/.github/actions/your_action@v1

Your ``PROJECT_CONFIG`` needs to have the flag ``create_major_version_tags=True``.
Your ``PROJECT_CONFIG`` needs to have the flag
``create_major_version_tags=True``.

What to do if the release failed?
Updating Dependencies After Having Prepared the Release
+++++++++++++++++++++++++++++++++++++++++++++++++++++++

If you need to update some more dependencies after running the nox session
``release:prepare`` you can update them in the changelog by running the nox
session ``release:update``.


What to do if the Release Failed?
+++++++++++++++++++++++++++++++++

The release failed during pre-release checks
The Release Failed During Pre-Release Checks
--------------------------------------------

#. Delete the local tag
Expand All @@ -68,13 +83,14 @@ The release failed during pre-release checks
#. Start the release process from the beginning


One of the release steps failed (Partial Release)
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 was interrupted.
**Scenario**: Publishing of the release on GitHub was successful but
during the PyPi release, the upload step was interrupted.

**Solution**: Manually push the package to PyPi
10 changes: 5 additions & 5 deletions doc/user_guide/features/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Features
.. toctree::
:maxdepth: 2

metrics/collecting_metrics
creating_a_release
documentation/index
git_hooks/index
github_workflows/index
formatting_code/index
github_workflows/index
documentation/index
creating_a_release
managing_dependencies
git_hooks/index
metrics/collecting_metrics

Uniform Project Layout
----------------------
Expand Down
33 changes: 22 additions & 11 deletions exasol/toolbox/nox/_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,27 @@ def _create_parser() -> argparse.ArgumentParser:
"-t",
"--type",
type=ReleaseTypes,
help="specifies which type of upgrade is to be performed",
help="Specifies which part of the version number to increase.",
required=True,
default=argparse.SUPPRESS,
)
parser.add_argument(
"--no-add",
default=False,
action="store_true",
help="Neither add nor commit the changes",
help="Neither add nor commit the changes.",
)
parser.add_argument(
"--no-branch",
default=False,
action="store_true",
help="Do not create a branch to commit the changes on",
help="Do not create a branch to commit the changes on.",
)
parser.add_argument(
"--no-pr",
default=False,
action="store_true",
help="Do not create a pull request for the changes",
help="Do not create a pull request for the changes.",
)
return parser

Expand All @@ -64,6 +64,14 @@ def _update_project_version(session: Session, version: Version) -> Version:
return version


def _get_changelogs(version: Version) -> Changelogs:
return Changelogs(
changes_path=PROJECT_CONFIG.documentation_path / "changes",
root_path=PROJECT_CONFIG.root_path,
version=version,
)


def _add_files_to_index(session: Session, files: list[Path]) -> None:
for file in files:
session.run("git", "add", f"{file}")
Expand Down Expand Up @@ -124,13 +132,9 @@ def prepare_release(session: Session) -> None:

_ = _update_project_version(session, new_version)

changelogs = Changelogs(
changes_path=PROJECT_CONFIG.documentation_path / "changes",
root_path=PROJECT_CONFIG.root_path,
version=new_version,
changed_files = (
_get_changelogs(version=new_version).prepare_release().get_changed_files()
)
changelogs.update_changelogs_for_release()
changed_files = changelogs.get_changed_files()

pm = NoxTasks.plugin_manager(PROJECT_CONFIG)
pm.hook.prepare_release_update_version(
Expand Down Expand Up @@ -164,7 +168,14 @@ def prepare_release(session: Session) -> None:
)


@nox.session(name="release:update", python=False)
def release_update(session: Session) -> None:
"""Update the changelog of the release already prepared."""
version = Version.from_poetry()
_get_changelogs(version).update_latest()


@nox.session(name="release:trigger", python=False)
def trigger_release(session: Session) -> None:
"""trigger an automatic project release"""
"""Trigger an automatic project release."""
print(f"new version: {_trigger_release(PROJECT_CONFIG)}")
51 changes: 37 additions & 14 deletions exasol/toolbox/util/release/changelog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from collections import OrderedDict
from datetime import datetime
from inspect import cleandoc
Expand All @@ -19,6 +20,8 @@
## Summary
""") + "\n"

DEPENDENCY_UPDATES = "## Dependency Updates\n"


class Changelogs:
def __init__(self, changes_path: Path, root_path: Path, version: Version) -> None:
Expand All @@ -41,6 +44,7 @@ def _create_new_unreleased(self):
"""
Write a new unreleased changelog file.
"""

self.unreleased_md.write_text(UNRELEASED_INITIAL_CONTENT)

def _create_versioned_changelog(self, unreleased_content: str) -> None:
Expand All @@ -50,30 +54,30 @@ def _create_versioned_changelog(self, unreleased_content: str) -> None:
Args:
unreleased_content: the content of the (not yet versioned) changes
"""
header = f"# {self.version} - {datetime.today().strftime('%Y-%m-%d')}"

dependency_content = ""
if dependency_changes := self._describe_dependency_changes():
dependency_content = f"## Dependency Updates\n{dependency_changes}"

template = cleandoc(f"{header}\n\n{unreleased_content}\n{dependency_content}")
header = f"# {self.version} - {datetime.today().strftime('%Y-%m-%d')}"
dependency_changes = self._report_dependency_changes()
template = cleandoc(f"{header}\n\n{unreleased_content}\n{dependency_changes}")
self.versioned_changelog_md.write_text(template + "\n")

def _extract_unreleased_notes(self) -> str:
"""
Extract (not yet versioned) changes from `unreleased.md`.
"""

with self.unreleased_md.open(mode="r", encoding="utf-8") as f:
# skip header when reading in file, as contains # Unreleased
lines = f.readlines()[1:]
unreleased_content = cleandoc("".join(lines))
return unreleased_content + "\n"

def _describe_dependency_changes(self) -> str:
def _dependency_changes(self) -> str:
"""
Describe the dependency changes between the latest tag and the current version
for use in the versioned changes file.
Return the dependency changes between the latest tag and the
current version for use in the versioned changes file in markdown
format. If there are no changes, return an empty string.
"""

try:
previous_dependencies_in_groups = get_dependencies_from_latest_tag(
root_path=self.root_path
Expand Down Expand Up @@ -112,6 +116,7 @@ def _sort_groups(groups: set[str]) -> list[str]:
- `main` group should always be first
- remaining groups are sorted alphabetically
"""

main = "main"
if main not in groups:
# sorted converts set to list
Expand All @@ -120,10 +125,10 @@ def _sort_groups(groups: set[str]) -> list[str]:
# sorted converts set to list
return [main] + sorted(remaining_groups)

def _update_changelog_table_of_contents(self) -> None:
def _update_table_of_contents(self) -> None:
"""
Read in existing `changelog.md` and append to appropriate sections
before writing out to again.
Read the existing `changelog.md`, append the latest changes file
to the relevant sections, and write the updated changelog.md again.
"""
updated_content = []
with self.changelog_md.open(mode="r", encoding="utf-8") as f:
Expand All @@ -142,7 +147,24 @@ def _update_changelog_table_of_contents(self) -> None:
def get_changed_files(self) -> list[Path]:
return [self.unreleased_md, self.versioned_changelog_md, self.changelog_md]

def update_changelogs_for_release(self) -> None:
def _report_dependency_changes(self) -> str:
if changes := self._dependency_changes():
return f"{DEPENDENCY_UPDATES}{changes}"
return ""

def update_latest(self) -> Changelogs:
"""
Update the updated dependencies in the latest versioned changelog.
"""

content = self.versioned_changelog_md.read_text()
flags = re.DOTALL | re.MULTILINE
stripped = re.sub(r"^{DEPENDENCY_UPDATES}.*", "", content, flags=flags)
dependency_changes = self._report_dependency_changes()
self.versioned_changelog_md.write_text(f"{stripped}\n{dependency_changes}")
return self

def prepare_release(self) -> Changelogs:
"""
Rotates the changelogs as is needed for a release.

Expand All @@ -157,4 +179,5 @@ def update_changelogs_for_release(self) -> None:

# update other changelogs now that versioned changelog exists
self._create_new_unreleased()
self._update_changelog_table_of_contents()
self._update_table_of_contents()
return self
Loading