Skip to content

Refactoring/498 centralize changelog code and preparation steps #499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ea6693c
Add git utility class & test
ArBridgeman Jul 17, 2025
8501403
Switch from decorator to annotated type so can be shared later
ArBridgeman Jul 17, 2025
ae1a58d
Remove conflicting git files as they are no longer used in code
ArBridgeman Jul 17, 2025
6706fa9
Extract combined functionality into get_dependencies & modify for get…
ArBridgeman Jul 17, 2025
75093f4
Improve name of function & switch to verb tense without s
ArBridgeman Jul 17, 2025
f7b866a
Move create_and_switch_to_branch to util.git
ArBridgeman Jul 17, 2025
9666080
Remove unused function _is_valid_version
ArBridgeman Jul 17, 2025
f608f7f
Add depth to see if resolves issues
ArBridgeman Jul 17, 2025
c83e75b
Refactor Changelog modifications so clearer order of operations and h…
ArBridgeman Jul 17, 2025
d12c2a5
Add changelog entry
ArBridgeman Jul 17, 2025
ab80fc1
Fix variable name & value as drifted from existing implementation
ArBridgeman Jul 17, 2025
73302d7
Fix comment
ArBridgeman Jul 17, 2025
247042d
Switch to AfterValidator so pre-validation works as expected
ArBridgeman Jul 18, 2025
734636a
Switch from string that we split into a list to a most sustainable li…
ArBridgeman Jul 21, 2025
7c23d20
Rename UNRELEASED_TEXT to UNRELEASED_INITIAL_CONTENT
ArBridgeman Jul 21, 2025
8f2b60b
Update docstring per review to clarify new file
ArBridgeman Jul 21, 2025
1845d48
Remove unused tmp_path from fixtures
ArBridgeman Jul 21, 2025
03b35f2
Set scope more explicitly and write comment once on test class
ArBridgeman Jul 21, 2025
8349817
Rename test variables and put into class so relationship is clearer
ArBridgeman Jul 21, 2025
6c66934
Update comment to be more explicit
ArBridgeman Jul 21, 2025
48e9a07
Update PTB dependencies
ArBridgeman Jul 21, 2025
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
2 changes: 2 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ jobs:
steps:
- name: SCM Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Python & Poetry Environment
uses: ./.github/actions/python-environment
Expand Down
4 changes: 4 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Unreleased

## Refactoring

* #498: Centralized changelog code relevant for `release:trigger` & robustly tested
14 changes: 0 additions & 14 deletions exasol/toolbox/git.py

This file was deleted.

13 changes: 3 additions & 10 deletions exasol/toolbox/nox/_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
licenses,
packages_to_markdown,
)
from exasol.toolbox.util.dependencies.poetry_dependencies import (
PoetryDependencies,
PoetryToml,
)
from exasol.toolbox.util.dependencies.poetry_dependencies import get_dependencies


class Audit:
Expand Down Expand Up @@ -86,12 +83,8 @@ def run(self, session: Session) -> None:

@nox.session(name="dependency:licenses", python=False)
def dependency_licenses(session: Session) -> None:
"""returns the packages and their licenses"""
working_directory = Path()
poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory)
dependencies = PoetryDependencies(
groups=poetry_dep.groups, working_directory=working_directory
).direct_dependencies
"""Return the packages with their licenses"""
dependencies = get_dependencies(working_directory=Path())
package_infos = licenses()
print(packages_to_markdown(dependencies=dependencies, packages=package_infos))

Expand Down
52 changes: 14 additions & 38 deletions exasol/toolbox/nox/_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@
_version,
)
from exasol.toolbox.nox.plugin import NoxTasks
from exasol.toolbox.release import (
extract_release_notes,
new_changelog,
new_changes,
new_unreleased,
)
from exasol.toolbox.util.git import Git
from exasol.toolbox.util.release.changelog import Changelogs
from exasol.toolbox.util.version import (
ReleaseTypes,
Version,
Expand Down Expand Up @@ -61,32 +57,12 @@ def _create_parser() -> argparse.ArgumentParser:
return parser


def _is_valid_version(old: Version, new: Version) -> bool:
return new >= old


def _update_project_version(session: Session, version: Version) -> Version:
session.run("poetry", "version", f"{version}")
_version(session, Mode.Fix)
return version


def _update_changelog(version: Version) -> tuple[Path, Path, Path]:
unreleased = Path(PROJECT_CONFIG.root) / "doc" / "changes" / "unreleased.md"
changelog = Path(PROJECT_CONFIG.root) / "doc" / "changes" / f"changes_{version}.md"
changes = Path(PROJECT_CONFIG.root) / "doc" / "changes" / f"changelog.md"

changelog_content = extract_release_notes(unreleased)
changelog.write_text(new_changelog(version, changelog_content))

unreleased.write_text(new_unreleased())

changes_content = new_changes(changes, version)
changes.write_text(changes_content)

return changelog, changes, unreleased


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 @@ -131,40 +107,40 @@ def run(*args: str):
@nox.session(name="release:prepare", python=False)
def prepare_release(session: Session) -> None:
"""
Prepares the project for a new release.
Prepare the project for a new release.
"""
parser = _create_parser()
args = parser.parse_args(session.posargs)

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}")

pm = NoxTasks.plugin_manager(PROJECT_CONFIG)
Git.create_and_switch_to_branch(f"release/prepare-{new_version}")

_ = _update_project_version(session, new_version)
changelog, changes, unreleased = _update_changelog(new_version)

changelogs = Changelogs(
changes_path=PROJECT_CONFIG.doc / "changes", version=new_version
)
changelogs.update_changelogs_for_release()
changed_files = changelogs.get_changed_files()

pm = NoxTasks.plugin_manager(PROJECT_CONFIG)
pm.hook.prepare_release_update_version(
session=session, config=PROJECT_CONFIG, version=new_version
)

if args.no_add:
return

files = [
changelog,
unreleased,
changes,
changed_files += [
PROJECT_CONFIG.root / "pyproject.toml",
PROJECT_CONFIG.version_file,
]
results = pm.hook.prepare_release_add_files(session=session, config=PROJECT_CONFIG)
files += [f for plugin_response in results for f in plugin_response]
changed_files += [f for plugin_response in results for f in plugin_response]
_add_files_to_index(
session,
files,
changed_files,
)
session.run("git", "commit", "-m", f"Prepare release {new_version}")

Expand Down
85 changes: 0 additions & 85 deletions exasol/toolbox/release/__init__.py

This file was deleted.

23 changes: 22 additions & 1 deletion exasol/toolbox/util/dependencies/poetry_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import subprocess
import tempfile
from pathlib import Path
from typing import Optional

Expand All @@ -12,6 +13,7 @@
from tomlkit import TOMLDocument

from exasol.toolbox.util.dependencies.shared_models import Package
from exasol.toolbox.util.git import Git


class PoetryGroup(BaseModel):
Expand All @@ -21,6 +23,7 @@ class PoetryGroup(BaseModel):
toml_section: Optional[str]


PYPROJECT_TOML = "pyproject.toml"
TRANSITIVE_GROUP = PoetryGroup(name="transitive", toml_section=None)


Expand All @@ -31,7 +34,7 @@ class PoetryToml(BaseModel):

@classmethod
def load_from_toml(cls, working_directory: Path) -> PoetryToml:
file_path = working_directory / "pyproject.toml"
file_path = working_directory / PYPROJECT_TOML
if not file_path.exists():
raise ValueError(f"File not found: {file_path}")

Expand Down Expand Up @@ -142,3 +145,21 @@ def all_dependencies(self) -> dict[str, list[Package]]:
transitive_dependencies.append(dep)

return direct_dependencies | {TRANSITIVE_GROUP.name: transitive_dependencies}


def get_dependencies(working_directory: Path) -> dict[str, list[Package]]:
poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory)
return PoetryDependencies(
groups=poetry_dep.groups, working_directory=working_directory
).direct_dependencies


def get_dependencies_from_latest_tag() -> dict[str, list[Package]]:
latest_tag = Git.get_latest_tag()
with tempfile.TemporaryDirectory() as path:
tmpdir = Path(path)

Git.copy_remote_file_locally(latest_tag, "poetry.lock", tmpdir)
Git.copy_remote_file_locally(latest_tag, PYPROJECT_TOML, tmpdir)

return get_dependencies(working_directory=tmpdir)
12 changes: 6 additions & 6 deletions exasol/toolbox/util/dependencies/shared_models.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from __future__ import annotations

from typing import Annotated

from packaging.version import Version
from pydantic import (
AfterValidator,
BaseModel,
ConfigDict,
field_validator,
)

VERSION_TYPE = Annotated[str, AfterValidator(lambda v: Version(v))]


class Package(BaseModel):
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

name: str
version: Version

@field_validator("version", mode="before")
def convert_version(cls, v: str) -> Version:
return Version(v)
version: VERSION_TYPE

@property
def normalized_name(self) -> str:
Expand Down
50 changes: 50 additions & 0 deletions exasol/toolbox/util/git.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import subprocess # nosec
from functools import wraps
from pathlib import Path


def run_command(func):
@wraps(func)
def wrapper(*args, **kwargs) -> str:
command_list = func(*args, **kwargs)
output = subprocess.run(
command_list, capture_output=True, text=True, check=True
) # nosec
return output.stdout.strip()

return wrapper


class Git:
@staticmethod
@run_command
def get_latest_tag():
"""
Get the latest tag from the git repository.
"""
return ["git", "describe", "--tags", "--abbrev=0"]

@staticmethod
@run_command
def read_file_from_tag(tag: str, remote_file: str):
"""
Read the contents of the specified file `remote_file` at the point in time
specified by git tag `tag`.
"""
return ["git", "cat-file", "blob", f"{tag}:{remote_file}"]

@staticmethod
def copy_remote_file_locally(
tag: str, remote_file: str, destination_directory: Path
) -> None:
"""
Copy the contents of the specified file `remote_file` at the point in time
specified by git tag `tag` and copy it into the local `destination_directory/remote_file`.
"""
contents = Git.read_file_from_tag(tag=tag, remote_file=remote_file)
(destination_directory / remote_file).write_text(contents)

@staticmethod
@run_command
def create_and_switch_to_branch(branch_name: str):
return ["git", "switch", "-c", branch_name]
Empty file.
Loading