Merge branch 'refactor-release-domain' into 'ddd-intro'

Refactor release domain

See merge request domaindrivenarchitecture/dda-devops-build!6
This commit is contained in:
Michael Jerger 2023-04-22 12:40:40 +00:00
commit 8e2ca4e420
15 changed files with 145 additions and 124 deletions

View file

@ -32,8 +32,19 @@ classDiagram
ipv6
}
class Release {
main_branch
config_file
}
class ReleaseContext {
release_type
version
current_branch
}
C4k *-- DnsRecord
Image *-- Devops
Release *-- "0..1" ReleaseContext
```

View file

@ -20,6 +20,6 @@ from .devops_build import DevopsBuild, create_devops_build_config, get_devops_bu
from .credential import gopass_password_from_path, gopass_field_from_path
from .release_mixin import ReleaseMixin
from .domain import Validateable, DnsRecord, Devops, Image
from .domain import Validateable, DnsRecord, Devops, Image, Release, ReleaseContext
__version__ = "${version}"

View file

@ -1,16 +1,16 @@
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseRepository, GitApi
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseContextRepository, GitApi
from src.main.python.ddadevops.domain import Version
class PrepareReleaseService():
def __init__(self, release_repo: ReleaseRepository):
def __init__(self, release_repo: ReleaseContextRepository):
self.release_repo = release_repo
self.release = release_repo.get_release() # TODO: make stateless: argument to receiving func
self.git_api = GitApi()
def __write_and_commit_version(self, version: Version, commit_message: str):
self.release.is_valid(self.release_repo.main_branch)
self.release.is_valid()
self.release_repo.version_repository.write_file(version.get_version_string())
self.git_api.add_file(self.release_repo.version_repository.file)
@ -24,7 +24,7 @@ class PrepareReleaseService():
class TagAndPushReleaseService():
def __init__(self, git_api: GitApi, release_repo: ReleaseRepository):
def __init__(self, git_api: GitApi, release_repo: ReleaseContextRepository):
self.git_api = git_api
self.release_repo = release_repo
self.release = release_repo.get_release() # TODO: make stateless: argument to receiving func

View file

@ -1,4 +1,4 @@
from .common import Validateable, DnsRecord, Devops
from .image import Image
from .c4k import C4k
from .release import Release, ReleaseConfig, ReleaseType, Version, EnvironmentKeys
from .release import Release, ReleaseContext, ReleaseType, Version, EnvironmentKeys

View file

@ -9,7 +9,7 @@ class Validateable:
def __validate_is_not_empty__(self, field_name: str) -> List[str]:
value = self.__dict__[field_name]
if value is None or value == "":
return [f"Field '{field_name}' may not be empty."]
return [f"Field '{field_name}' must not be empty."]
else:
return []

View file

@ -63,39 +63,60 @@ class Version():
bump_version.increment(ReleaseType.BUMP)
return bump_version
class ReleaseConfig(Validateable):
def __init__(
self,
main_branch: str,
config_file: str,
devops: Devops,
release_version: Optional[str | None] = None,
bump_version: Optional[str | None] = None
):
self.main_branch = main_branch
self.config_file = config_file
# TODO: this information may be transient?
self.release_version = release_version
self.bump_version = bump_version
self.devops = devops
class Release(Validateable):
def __init__(self, release_type: ReleaseType | None, version: Version, current_branch: str, main_branch: str):
class ReleaseContext(Validateable):
def __init__(self, release_type: ReleaseType | None, version: Version, current_branch: str):
self.release_type = release_type
self.version = version
self.current_branch = current_branch
self.main_branch = main_branch
def release_version(self):
def release_version(self) -> Version:
return self.version.create_release_version(self.release_type)
def bump_version(self):
def bump_version(self) -> Version:
return self.release_version().create_bump_version()
def validate(self):
result = []
if self.release_type is not None and self.main_branch != self.current_branch:
result += self.__validate_is_not_empty__("release_type")
result += self.__validate_is_not_empty__("version")
result += self.__validate_is_not_empty__("current_branch")
return result
def validate_branch(self, main_branch: str):
result = []
if self.release_type is not None and main_branch != self.current_branch:
result.append(f"Releases are allowed only on {main_branch}")
return result
class Release(Validateable):
def __init__(
self,
devops: Devops,
main_branch: str,
config_file: str,
):
self.devops = devops
self.main_branch = main_branch
self.config_file = config_file
self.release_context = None
def set_release_context(self, set_release_context: ReleaseContext) -> None:
self.release_context = set_release_context
def release_version(self):
return self.release_context.release_version()
def bump_version(self):
return self.release_context.bump_version()
def validate(self):
result = []
result += self.__validate_is_not_empty__("main_branch")
result += self.__validate_is_not_empty__("config_file")
result += self.__validate_is_not_empty__("release_context")
if self.release_context is not None:
result += self.release_context.validate()
result += self.release_context.validate_branch(self.main_branch)
return result

View file

@ -4,7 +4,7 @@ from os import chmod
from subprocess import run
from pkg_resources import resource_string
import yaml
from ..domain import Devops, Image, C4k, ReleaseConfig
from ..domain import Devops, Image, C4k, Release
from ..python_util import execute
@ -30,10 +30,10 @@ class ProjectRepository:
def set_c4k(self, project, build: C4k):
project.set_property("c4k_build", build)
def get_release(self, project) -> ReleaseConfig:
def get_release(self, project) -> Release:
return project.get_property("release_build")
def set_release(self, project, build: ReleaseConfig):
def set_release(self, project, build: Release):
project.set_property("release_build", build)

View file

@ -1,2 +1,2 @@
from .infrastructure_api import FileHandler, EnvironmentApi, GitApi, JsonFileHandler, GradleFileHandler, PythonFileHandler, ClojureFileHandler
from .repo import VersionRepository, ReleaseRepository, ReleaseTypeRepository
from .repo import VersionRepository, ReleaseContextRepository, ReleaseTypeRepository

View file

@ -1,6 +1,6 @@
from typing import Optional
from src.main.python.ddadevops.domain import (
Release,
ReleaseContext,
Version,
ReleaseType,
EnvironmentKeys,
@ -106,7 +106,8 @@ class ReleaseTypeRepository:
raise ValueError("No valid api passed to ReleaseTypeRepository")
class ReleaseRepository:
# TODO: Repo has state & repository should exist only for AggregateRoot
class ReleaseContextRepository:
def __init__(
self,
version_repository: VersionRepository,
@ -117,9 +118,13 @@ class ReleaseRepository:
self.release_type_repository = release_type_repository
self.main_branch = main_branch
def get_release(self) -> Release:
return Release(
def get_release(self) -> ReleaseContext:
result = ReleaseContext(
self.release_type_repository.get_release_type(),
self.version_repository.get_version(),
self.main_branch,
)
if not result.is_valid():
issues = '\n'.join.result.validate()
raise ValueError(f"invalid release: {issues}")
return result

View file

@ -1,41 +1,14 @@
from typing import Optional
from pybuilder.core import Project
from src.main.python.ddadevops.devops_build import DevopsBuild
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseRepository, ReleaseTypeRepository, VersionRepository, GitApi, EnvironmentApi
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseContextRepository, ReleaseTypeRepository, VersionRepository, GitApi, EnvironmentApi
from src.main.python.ddadevops.application import PrepareReleaseService, TagAndPushReleaseService
from src.main.python.ddadevops.domain import ReleaseConfig, EnvironmentKeys
from src.main.python.ddadevops.domain import Release, EnvironmentKeys
# TODO: remove the config creation
def create_release_mixin_config(config_file, main_branch) -> dict:
config = {}
config.update(
{'ReleaseMixin':
{'main_branch': main_branch,
'config_file': config_file}})
return config
# TODO: remove the config creation
def add_versions(config, release_version, bump_version) -> dict:
config['ReleaseMixin'].update(
{'release_version': release_version,
'bump_version': bump_version})
return config
# TODO: remove the config argument
class ReleaseMixin(DevopsBuild):
def __init__(self, project: Project, release_config: Optional[ReleaseConfig] = None):
if not release_config:
if not config:
raise ValueError("Release parameters could not be set.")
super().__init__(project, config=config)
release_mixin_config = config['ReleaseMixin']
release_config = ReleaseConfig(
main_branch = release_mixin_config['main_branch'],
config_file = release_mixin_config['config_file'],
devops=self.repo.get_devops(project)
)
else:
super().__init__(project, devops=release_config.devops)
self.repo.set_release(self.project, release_config)
def __init__(self, project: Project, release: Release):
super().__init__(project, devops=release.devops)
self.repo.set_release(self.project, release)
git_api = GitApi()
environment_api = EnvironmentApi()
@ -47,8 +20,8 @@ class ReleaseMixin(DevopsBuild):
else:
release_type_repo = ReleaseTypeRepository.from_git(git_api)
version_repo = VersionRepository(release_config.config_file)
release_repo = ReleaseRepository(version_repo, release_type_repo, release_config.main_branch)
version_repo = VersionRepository(release.config_file)
release_repo = ReleaseContextRepository(version_repo, release_type_repo, release.main_branch)
self.prepare_release_service = PrepareReleaseService(release_repo)
self.tag_and_push_release_service = TagAndPushReleaseService(git_api, release_repo)

View file

@ -1,9 +1,11 @@
from pybuilder.core import Project
from pathlib import Path
from src.main.python.ddadevops.domain.common import (
Validateable,
DnsRecord,
Devops,
)
from src.main.python.ddadevops.domain import Version, ReleaseType, Release, ReleaseContext
from src.main.python.ddadevops.domain.image import Image
from src.main.python.ddadevops.domain.c4k import C4k
from src.main.python.ddadevops.c4k_mixin import add_c4k_mixin_config
@ -44,7 +46,7 @@ def test_should_validate_non_empty_others():
def test_validate_with_reason():
sut = MockValidateable(None)
assert sut.validate()[0] == "Field 'field' may not be empty."
assert sut.validate()[0] == "Field 'field' must not be empty."
def test_should_validate_DnsRecord():
@ -173,3 +175,52 @@ def test_c4k_build_should_calculate_command():
+ "/target/name/module/out_module.yaml"
== sut.command(devops)
)
def test_version(tmp_path: Path):
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.SNAPSHOT)
assert version.get_version_string() == "1.2.3-SNAPSHOT"
assert version.version_list == [1, 2, 3]
assert version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.BUMP)
assert version.get_version_string() == "1.2.4-SNAPSHOT"
assert version.version_list == [1, 2, 4]
assert version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.PATCH)
assert version.get_version_string() == "1.2.4"
assert version.version_list == [1, 2, 4]
assert not version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.MINOR)
assert version.get_version_string() == "1.3.0"
assert version.version_list == [1, 3, 0]
assert not version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.MAJOR)
assert version.get_version_string() == "2.0.0"
assert version.version_list == [2, 0, 0]
assert not version.is_snapshot
def test_release_context(tmp_path):
version = Version(tmp_path, [1, 2, 3])
release = ReleaseContext(ReleaseType.MINOR, version, "main")
release_version = release.release_version()
assert release_version.get_version_string() in '1.3.0'
bump_version = release.bump_version()
assert bump_version.get_version_string() in "1.3.1-SNAPSHOT"
def test_release(tmp_path):
devops = Devops(stage="test", project_root_path="", module="module", name="name")
sut = Release(devops, "main", "config_file.json")
assert not sut.is_valid()
sut.set_release_context(ReleaseContext(ReleaseType.MINOR, Version("id", [1,2,3]), "main"))
assert sut.is_valid()

View file

@ -1,5 +1,7 @@
from src.main.python.ddadevops.domain import ReleaseType
# TODO: Domain never should be needed to mock! Why do you not yust create the needed object?
class MockVersion():
def __init__(self, id = None, version_list = None):
@ -33,9 +35,9 @@ class MockRelease():
def bump_version(self):
return self.release_version().create_bump_version()
def validate(self, main_branch):
def validate(self):
self.validate_count += 1
return []
def is_valid(self, main_branch):
return len(self.validate(main_branch)) < 1
def is_valid(self):
return len(self.validate()) < 1

View file

@ -1,43 +0,0 @@
from pathlib import Path
from src.main.python.ddadevops.domain import Version, ReleaseType, Release
def test_version(tmp_path: Path):
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.SNAPSHOT)
assert version.get_version_string() == "1.2.3-SNAPSHOT"
assert version.version_list == [1, 2, 3]
assert version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.BUMP)
assert version.get_version_string() == "1.2.4-SNAPSHOT"
assert version.version_list == [1, 2, 4]
assert version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.PATCH)
assert version.get_version_string() == "1.2.4"
assert version.version_list == [1, 2, 4]
assert not version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.MINOR)
assert version.get_version_string() == "1.3.0"
assert version.version_list == [1, 3, 0]
assert not version.is_snapshot
version = Version(tmp_path, [1, 2, 3])
version.increment(ReleaseType.MAJOR)
assert version.get_version_string() == "2.0.0"
assert version.version_list == [2, 0, 0]
assert not version.is_snapshot
def test_release(tmp_path):
version = Version(tmp_path, [1, 2, 3])
release = Release(ReleaseType.MINOR, version, "main")
release_version = release.release_version()
assert release_version.get_version_string() in '1.3.0'
bump_version = release.bump_version()
assert bump_version.get_version_string() in "1.3.1-SNAPSHOT"

View file

@ -1,5 +1,5 @@
from src.main.python.ddadevops.domain import ReleaseType
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseTypeRepository, VersionRepository, ReleaseRepository
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseTypeRepository, VersionRepository, ReleaseContextRepository
from .mock_infrastructure_api import MockGitApi, MockEnvironmentApi
from .helper import Helper
@ -22,7 +22,8 @@ def test_release_repository(tmp_path):
release_type_repo = ReleaseTypeRepository.from_git(MockGitApi('MINOR test'))
# test
sut = ReleaseRepository(version_repo, release_type_repo, 'main')
sut = ReleaseContextRepository(version_repo, release_type_repo, 'main')
release = sut.get_release()
assert release is not None

View file

@ -4,7 +4,7 @@ from pybuilder.core import Project
from src.main.python.ddadevops.release_mixin import ReleaseMixin
from src.main.python.ddadevops.infrastructure.release_mixin import GitApi, EnvironmentApi
from src.main.python.ddadevops.domain import Devops, ReleaseConfig
from src.main.python.ddadevops.domain import Devops, ReleaseContext, Release
from .helper import Helper
@ -23,8 +23,8 @@ class MyBuild(ReleaseMixin):
def initialize_with_object(project, CONFIG_FILE):
project.build_depends_on('ddadevops>=3.1.2')
devops = Devops(STAGE, PROJECT_ROOT_PATH, MODULE, "release_test", BUILD_DIR_NAME)
release_config = ReleaseConfig(MAIN_BRANCH, CONFIG_FILE, devops)
build = MyBuild(project, release_config=release_config)
release = Release(devops, MAIN_BRANCH, CONFIG_FILE)
build = MyBuild(project, release)
return build
def test_release_mixin_git(tmp_path: Path, monkeypatch: pt.MonkeyPatch):