From 420322f2de42da734b3bd8ab62afd0d2c4a66f20 Mon Sep 17 00:00:00 2001 From: Guillaume Charbonnier Date: Fri, 16 Feb 2024 08:52:38 +0100 Subject: [PATCH 1/5] added new option to filter on branch --- .../hexagon/entities/strategy/__init__.py | 9 +- .../hexagon/entities/strategy/application.py | 58 ----- .../hexagon/entities/strategy/policy.py | 41 +++- .../entities/strategy/release_strategy.py | 67 ++++- src/releaser/hexagon/ports/git_reader.py | 11 + .../hexagon/services/manifest_generator.py | 15 +- src/releaser/infra/git_reader/stub.py | 3 + src/releaser/infra/git_reader/subprocess.py | 4 + tests/test_hexagon/test_manifest_generator.py | 231 +++++++++++------- 9 files changed, 268 insertions(+), 171 deletions(-) delete mode 100644 src/releaser/hexagon/entities/strategy/application.py diff --git a/src/releaser/hexagon/entities/strategy/__init__.py b/src/releaser/hexagon/entities/strategy/__init__.py index 5e54061..6975342 100644 --- a/src/releaser/hexagon/entities/strategy/__init__.py +++ b/src/releaser/hexagon/entities/strategy/__init__.py @@ -1,9 +1,13 @@ from __future__ import annotations -from .application import Application, ApplicationReleaseStrategy from .image import Image from .policy import CommitMsgMatchPolicy, GitCommitShaTag, LiteralTag, VersionTag -from .release_strategy import ReleaseStrategy +from .release_strategy import ( + Application, + ApplicationReleaseStrategy, + ReleaseStrategy, + Rule, +) __all__ = [ "Application", @@ -14,4 +18,5 @@ "VersionTag", "LiteralTag", "Image", + "Rule", ] diff --git a/src/releaser/hexagon/entities/strategy/application.py b/src/releaser/hexagon/entities/strategy/application.py deleted file mode 100644 index c3c40a3..0000000 --- a/src/releaser/hexagon/entities/strategy/application.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from .image import Image -from .policy import CommitMsgMatchPolicy - - -@dataclass -class Application: - """An application within a development repository.""" - - on_commit_msg: list[CommitMsgMatchPolicy] | None = None - """The tags to match for the HEAD commit.""" - - images: list[Image] | None = None - """The images produced by the application. - - Images must be a list of OCI-compliant image repositories. - """ - - @classmethod - def parse_dict(cls, data: dict[str, Any]) -> "Application": - """Parse an application from a dictionary.""" - policies: list[Any] | dict[str, Any] = data.get("on_commit_msg", []) - if isinstance(policies, list): - match_policies = [ - CommitMsgMatchPolicy.parse_dict(policy) for policy in policies - ] - else: - match_policies = [CommitMsgMatchPolicy.parse_dict(policies)] - return cls( - on_commit_msg=match_policies, - images=[_asimage(image) for image in data.get("images", [])], - ) - - -@dataclass -class ApplicationReleaseStrategy: - name: str - """The name of the application.""" - - match_commit_msg: list[CommitMsgMatchPolicy] - """The tags to match for the HEAD commit.""" - - images: list[Image] - """The images produced by the application. - - Images must be a list of OCI-compliant image repositories. - """ - - -def _asimage(value: str | dict[str, Any]) -> Image: - """Convert a value to an image.""" - if isinstance(value, str): - return Image(repository=value) - return Image.parse_dict(value) diff --git a/src/releaser/hexagon/entities/strategy/policy.py b/src/releaser/hexagon/entities/strategy/policy.py index 9d72824..b7e26d1 100644 --- a/src/releaser/hexagon/entities/strategy/policy.py +++ b/src/releaser/hexagon/entities/strategy/policy.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Literal @@ -93,11 +93,15 @@ def parse_dict(cls, data: dict[str, Any]) -> "CommitMsgMatchPolicy": return cls( match=_aslist(data["match"]), tags=[ - VersionTag.parse_dict(tag) - if tag.get("type") == "version" - else GitCommitShaTag.parse_dict(tag) - if tag.get("type") == "git_commit_sha" - else LiteralTag.parse_dict(tag) + ( + VersionTag.parse_dict(tag) + if tag.get("type") == "version" + else ( + GitCommitShaTag.parse_dict(tag) + if tag.get("type") == "git_commit_sha" + else LiteralTag.parse_dict(tag) + ) + ) for tag in data["tags"] ], filter=data.get("filter"), @@ -105,6 +109,31 @@ def parse_dict(cls, data: dict[str, Any]) -> "CommitMsgMatchPolicy": ) +@dataclass(frozen=True) +class Rule: + """A rule for a release strategy.""" + + branch: list[str] = field(default_factory=list) + """The branches to match for the HEAD commit message.""" + + commit_msg: list[CommitMsgMatchPolicy] = field(default_factory=list) + """The tags to match for the HEAD commit message. + These are global policies that apply to all applications. + """ + + @classmethod + def parse_dict(cls, data: dict[str, Any]) -> Rule: + """Parse a rule from a dictionary.""" + + return cls( + branch=_aslist(data.get("branch")), + commit_msg=[ + CommitMsgMatchPolicy.parse_dict(policy) + for policy in data.get("commit_msg", []) + ], + ) + + def _aslist(any: Any) -> list[Any]: """Convert a value to a list.""" if any is None: diff --git a/src/releaser/hexagon/entities/strategy/release_strategy.py b/src/releaser/hexagon/entities/strategy/release_strategy.py index fde1a57..c3dcb92 100644 --- a/src/releaser/hexagon/entities/strategy/release_strategy.py +++ b/src/releaser/hexagon/entities/strategy/release_strategy.py @@ -3,8 +3,50 @@ from dataclasses import dataclass, field from typing import Any -from .application import Application, ApplicationReleaseStrategy -from .policy import CommitMsgMatchPolicy +from .image import Image +from .policy import Rule + + +@dataclass +class Application: + """An application within a development repository.""" + + on: list[Rule] | None = None + """The tags to match for the HEAD commit.""" + + images: list[Image] | None = None + """The images produced by the application. + + Images must be a list of OCI-compliant image repositories. + """ + + @classmethod + def parse_dict(cls, data: dict[str, Any]) -> "Application": + """Parse an application from a dictionary.""" + policies: list[Any] | dict[str, Any] = data.get("on_commit_msg", []) + if isinstance(policies, list): + match_policies = [Rule.parse_dict(policy) for policy in policies] + else: + match_policies = [Rule.parse_dict(policies)] + return cls( + on=match_policies, + images=[_asimage(image) for image in data.get("images", [])], + ) + + +@dataclass +class ApplicationReleaseStrategy: + name: str + """The name of the application.""" + + on: list[Rule] + """The tags to match for the HEAD commit.""" + + images: list[Image] + """The images produced by the application. + + Images must be a list of OCI-compliant image repositories. + """ @dataclass(frozen=True) @@ -14,9 +56,9 @@ class ReleaseStrategy: applications: dict[str, Application] """The applications in the repository. Each application may define a custom release strategy.""" - on_commit_msg: list[CommitMsgMatchPolicy] = field(default_factory=list) - """The tags to match for the HEAD commit message. - These are global policies that apply to all applications. + on: list[Rule] = field(default_factory=list) + """The rules to set tags. + These are global rules that apply to all applications. """ @classmethod @@ -27,10 +69,7 @@ def parse_dict(cls, data: dict[str, Any]) -> "ReleaseStrategy": name: Application.parse_dict(application) for name, application in data.get("applications", {}).items() }, - on_commit_msg=[ - CommitMsgMatchPolicy.parse_dict(policy) - for policy in data.get("on_commit_msg", []) - ], + on=[Rule.parse_dict(rule) for rule in data.get("on", [])], ) def get_release_strategy_for_application( @@ -41,8 +80,7 @@ def get_release_strategy_for_application( if app_name == name: return ApplicationReleaseStrategy( name=app_name, - match_commit_msg=_aslist(application.on_commit_msg) - or self.on_commit_msg, + on=_aslist(application.on) or self.on, images=application.images or [], ) raise ValueError(f"Application {name} not found in release strategy") @@ -55,3 +93,10 @@ def _aslist(any: Any) -> list[Any]: if not isinstance(any, list): return [any] return any # type: ignore + + +def _asimage(value: str | dict[str, Any]) -> Image: + """Convert a value to an image.""" + if isinstance(value, str): + return Image(repository=value) + return Image.parse_dict(value) diff --git a/src/releaser/hexagon/ports/git_reader.py b/src/releaser/hexagon/ports/git_reader.py index d341a86..41ff7b8 100644 --- a/src/releaser/hexagon/ports/git_reader.py +++ b/src/releaser/hexagon/ports/git_reader.py @@ -11,6 +11,11 @@ def is_dirty(self) -> bool: """Check if the repository has uncommitted changes.""" raise NotImplementedError + @abc.abstractmethod + def read_current_branch(self) -> str: + """Read the name of the current branch.""" + raise NotImplementedError + @abc.abstractmethod def read_most_recent_commit_sha(self) -> str: """Read the SHA of the latest commit.""" @@ -33,3 +38,9 @@ def read_last_commit_message(self, depth: int, filter: str | None) -> str | None return commit_msg return None return history[0] + + def current_reference_matches(self, ref: list[str]) -> bool: + """Check if the current reference matches the given ref.""" + if not ref: + return True + return self.read_current_branch() in ref diff --git a/src/releaser/hexagon/services/manifest_generator.py b/src/releaser/hexagon/services/manifest_generator.py index 658e9ed..4ac0c9c 100644 --- a/src/releaser/hexagon/services/manifest_generator.py +++ b/src/releaser/hexagon/services/manifest_generator.py @@ -75,13 +75,16 @@ def _generate_application_tags( self, application: strategy.ApplicationReleaseStrategy ) -> Iterator[str]: """Generate the list of tags to release for an application.""" - for policy in application.match_commit_msg: - msg = self.git_reader.read_last_commit_message(policy.depth, policy.filter) - if not msg: + for rule in application.on: + if not self.git_reader.current_reference_matches(rule.branch): continue - if self._verify_commit_msg_match_against_policy(msg, policy): - for tag in policy.tags: - yield self._get_value_for_tag(tag) + for policy in rule.commit_msg: + msg = self.git_reader.read_last_commit_message(policy.depth, policy.filter) + if not msg: + continue + if self._verify_commit_msg_match_against_policy(msg, policy): + for tag in policy.tags: + yield self._get_value_for_tag(tag) def _verify_commit_msg_match_against_policy( self, commit_msg: str, policy: CommitMsgMatchPolicy diff --git a/src/releaser/infra/git_reader/stub.py b/src/releaser/infra/git_reader/stub.py index 5733ab9..1641964 100644 --- a/src/releaser/infra/git_reader/stub.py +++ b/src/releaser/infra/git_reader/stub.py @@ -11,6 +11,9 @@ def __init__(self) -> None: self._history: list[str] | None = None self._is_dirty: bool | None = None + def read_current_branch(self) -> str: + return "testing" + def is_dirty(self) -> bool: if self._is_dirty is None: raise RuntimeError("is_dirty not set in stub git reader") diff --git a/src/releaser/infra/git_reader/subprocess.py b/src/releaser/infra/git_reader/subprocess.py index 85d00fe..29c5c4b 100644 --- a/src/releaser/infra/git_reader/subprocess.py +++ b/src/releaser/infra/git_reader/subprocess.py @@ -18,6 +18,10 @@ def is_dirty(self) -> bool: process = subprocess.run(["git", "diff-index", "--quiet", "HEAD", "--"]) return process.returncode != 0 + def read_current_branch(self) -> str: + branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) + return branch.decode().strip() + def read_most_recent_commit_sha(self) -> str: long_sha = ( subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip() diff --git a/tests/test_hexagon/test_manifest_generator.py b/tests/test_hexagon/test_manifest_generator.py index 06bec5f..64a6a27 100644 --- a/tests/test_hexagon/test_manifest_generator.py +++ b/tests/test_hexagon/test_manifest_generator.py @@ -77,13 +77,18 @@ def test_with_single_image_and_literal_commit_msg_matcher( applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ) + ] + ) ], ) - }, + } ) ) # Act @@ -118,13 +123,19 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_all_matcher( applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.GitCommitShaTag(size=7)] - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ), + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ] + ) ], ) }, @@ -164,14 +175,19 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_single_matcher( applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), - strategy.CommitMsgMatchPolicy( - match=["this is the latest commit"], - tags=[strategy.GitCommitShaTag(size=7)], - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ), + strategy.CommitMsgMatchPolicy( + match=["this is the latest commit"], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ] + ) ], ) }, @@ -211,14 +227,19 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_single_matcher_no applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), - strategy.CommitMsgMatchPolicy( - match=["the expected pattern"], - tags=[strategy.GitCommitShaTag(size=7)], - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ), + strategy.CommitMsgMatchPolicy( + match=["the expected pattern"], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ] + ) ], ) }, @@ -256,14 +277,22 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_many_matchers( applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), - strategy.CommitMsgMatchPolicy( - match=["the first pattern", "the second pattern"], - tags=[strategy.GitCommitShaTag(size=7)], - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ), + strategy.CommitMsgMatchPolicy( + match=[ + "the first pattern", + "the second pattern", + ], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ] + ) ], ) }, @@ -305,16 +334,21 @@ def test_with_single_image_and_version_plus_git_tag_sha_policy_matcher_with_all_ applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), - strategy.CommitMsgMatchPolicy( - match=["*"], - tags=[ - strategy.VersionTag(), - strategy.GitCommitShaTag(size=7), - ], + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ), + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[ + strategy.VersionTag(), + strategy.GitCommitShaTag(size=7), + ], + ), + ] ), ], ) @@ -362,18 +396,23 @@ def test_with_single_image_and_version_plus_git_sha_tag_policy_policy_matcher_wi applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="head")] - ), - strategy.CommitMsgMatchPolicy( - match=["chore\\(release\\): bump to version"], - tags=[ - strategy.LiteralTag(value="edge"), - strategy.VersionTag(), - strategy.GitCommitShaTag(size=7), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[strategy.LiteralTag(value="head")], + ), + strategy.CommitMsgMatchPolicy( + match=["chore\\(release\\): bump to version"], + tags=[ + strategy.LiteralTag(value="edge"), + strategy.VersionTag(), + strategy.GitCommitShaTag(size=7), + ], + ), ], - ), + ) ], ) }, @@ -433,13 +472,17 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_all_matcher( images=[strategy.Image("test-image")], ) }, - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], - tags=[ - strategy.LiteralTag(value="next"), - strategy.GitCommitShaTag(size=7), - ], + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], + tags=[ + strategy.LiteralTag(value="next"), + strategy.GitCommitShaTag(size=7), + ], + ) + ] ) ], ) @@ -480,14 +523,18 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_single_matcher( images=[strategy.Image("test-image")], ) }, - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="next")] - ), - strategy.CommitMsgMatchPolicy( - match=["this is the latest commit"], - tags=[strategy.GitCommitShaTag(size=7)], - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], tags=[strategy.LiteralTag(value="next")] + ), + strategy.CommitMsgMatchPolicy( + match=["this is the latest commit"], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ], + ) ], ) ) @@ -527,14 +574,18 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_single_matcher_no images=[strategy.Image("test-image")], ) }, - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="next")] - ), - strategy.CommitMsgMatchPolicy( - match=["the expected pattern"], - tags=[strategy.GitCommitShaTag(size=7)], - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], tags=[strategy.LiteralTag(value="next")] + ), + strategy.CommitMsgMatchPolicy( + match=["the expected pattern"], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ], + ) ], ) ) @@ -572,14 +623,18 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_many_matchers( images=[strategy.Image("test-image")], ) }, - on_commit_msg=[ - strategy.CommitMsgMatchPolicy( - match=["*"], tags=[strategy.LiteralTag(value="next")] - ), - strategy.CommitMsgMatchPolicy( - match=["the first pattern", "the second pattern"], - tags=[strategy.GitCommitShaTag(size=7)], - ), + on=[ + strategy.Rule( + commit_msg=[ + strategy.CommitMsgMatchPolicy( + match=["*"], tags=[strategy.LiteralTag(value="next")] + ), + strategy.CommitMsgMatchPolicy( + match=["the first pattern", "the second pattern"], + tags=[strategy.GitCommitShaTag(size=7)], + ), + ], + ) ], ) ) From 6eada53c62c992243200632e680edc24ea6b67f9 Mon Sep 17 00:00:00 2001 From: Guillaume Charbonnier Date: Fri, 16 Feb 2024 08:52:53 +0100 Subject: [PATCH 2/5] formatted code using black --- src/releaser/cli/commands/analyze_manifest.py | 1 + src/releaser/cli/commands/bake_manifest.py | 1 + src/releaser/cli/commands/create_manifest.py | 1 + src/releaser/cli/commands/upload_manifest.py | 1 + src/releaser/hexagon/errors.py | 1 + src/releaser/hexagon/ports/git_reader.py | 1 + src/releaser/hexagon/ports/json_writer.py | 1 + src/releaser/hexagon/ports/strategy_reader.py | 1 + src/releaser/hexagon/ports/version_reader.py | 1 + src/releaser/hexagon/ports/webhook_client.py | 1 + src/releaser/hexagon/services/manifest_baker.py | 1 - src/releaser/hexagon/services/manifest_generator.py | 5 ++++- src/releaser/hexagon/services/manifest_notifier.py | 1 + src/releaser/infra/json_writer/stdout.py | 1 - 14 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/releaser/cli/commands/analyze_manifest.py b/src/releaser/cli/commands/analyze_manifest.py index a1b3e63..3f35c2d 100644 --- a/src/releaser/cli/commands/analyze_manifest.py +++ b/src/releaser/cli/commands/analyze_manifest.py @@ -1,4 +1,5 @@ """Create a release manifest.""" + from __future__ import annotations import argparse diff --git a/src/releaser/cli/commands/bake_manifest.py b/src/releaser/cli/commands/bake_manifest.py index 11ce9a3..dce2f14 100644 --- a/src/releaser/cli/commands/bake_manifest.py +++ b/src/releaser/cli/commands/bake_manifest.py @@ -1,4 +1,5 @@ """Create a release manifest.""" + from __future__ import annotations import argparse diff --git a/src/releaser/cli/commands/create_manifest.py b/src/releaser/cli/commands/create_manifest.py index c3760f8..a0aa784 100644 --- a/src/releaser/cli/commands/create_manifest.py +++ b/src/releaser/cli/commands/create_manifest.py @@ -1,4 +1,5 @@ """Create a release manifest.""" + from __future__ import annotations import argparse diff --git a/src/releaser/cli/commands/upload_manifest.py b/src/releaser/cli/commands/upload_manifest.py index 2617a59..c14aa2b 100644 --- a/src/releaser/cli/commands/upload_manifest.py +++ b/src/releaser/cli/commands/upload_manifest.py @@ -1,4 +1,5 @@ """Create a release manifest.""" + from __future__ import annotations import argparse diff --git a/src/releaser/hexagon/errors.py b/src/releaser/hexagon/errors.py index 324eca8..4e89e0a 100644 --- a/src/releaser/hexagon/errors.py +++ b/src/releaser/hexagon/errors.py @@ -1,4 +1,5 @@ """Custom errors for releaser hexagon.""" + from __future__ import annotations diff --git a/src/releaser/hexagon/ports/git_reader.py b/src/releaser/hexagon/ports/git_reader.py index 41ff7b8..f46140a 100644 --- a/src/releaser/hexagon/ports/git_reader.py +++ b/src/releaser/hexagon/ports/git_reader.py @@ -1,4 +1,5 @@ """This module defines the GitReader abstract base class.""" + from __future__ import annotations import abc diff --git a/src/releaser/hexagon/ports/json_writer.py b/src/releaser/hexagon/ports/json_writer.py index e5a4841..a90484c 100644 --- a/src/releaser/hexagon/ports/json_writer.py +++ b/src/releaser/hexagon/ports/json_writer.py @@ -1,4 +1,5 @@ """This module defines the JsonWriter abstract base class.""" + from __future__ import annotations import abc diff --git a/src/releaser/hexagon/ports/strategy_reader.py b/src/releaser/hexagon/ports/strategy_reader.py index f07e2f8..67ea6c4 100644 --- a/src/releaser/hexagon/ports/strategy_reader.py +++ b/src/releaser/hexagon/ports/strategy_reader.py @@ -1,4 +1,5 @@ """This module defines the StrategyReader abstract base class.""" + from __future__ import annotations import abc diff --git a/src/releaser/hexagon/ports/version_reader.py b/src/releaser/hexagon/ports/version_reader.py index aaa34c9..c9579d1 100644 --- a/src/releaser/hexagon/ports/version_reader.py +++ b/src/releaser/hexagon/ports/version_reader.py @@ -1,4 +1,5 @@ """This module defines the VersionReader abstract base class.""" + from __future__ import annotations import abc diff --git a/src/releaser/hexagon/ports/webhook_client.py b/src/releaser/hexagon/ports/webhook_client.py index 1203128..25b60bf 100644 --- a/src/releaser/hexagon/ports/webhook_client.py +++ b/src/releaser/hexagon/ports/webhook_client.py @@ -1,4 +1,5 @@ """This module defines the HttpClient abstract base class.""" + from __future__ import annotations import abc diff --git a/src/releaser/hexagon/services/manifest_baker.py b/src/releaser/hexagon/services/manifest_baker.py index a3956f0..3e7b3e8 100644 --- a/src/releaser/hexagon/services/manifest_baker.py +++ b/src/releaser/hexagon/services/manifest_baker.py @@ -1,6 +1,5 @@ """Service used to build manfest artefacts.""" - from dataclasses import dataclass from ..entities import bakery, strategy diff --git a/src/releaser/hexagon/services/manifest_generator.py b/src/releaser/hexagon/services/manifest_generator.py index 4ac0c9c..f211c4a 100644 --- a/src/releaser/hexagon/services/manifest_generator.py +++ b/src/releaser/hexagon/services/manifest_generator.py @@ -1,4 +1,5 @@ """Service used to generate manifest during a release.""" + from __future__ import annotations import re @@ -79,7 +80,9 @@ def _generate_application_tags( if not self.git_reader.current_reference_matches(rule.branch): continue for policy in rule.commit_msg: - msg = self.git_reader.read_last_commit_message(policy.depth, policy.filter) + msg = self.git_reader.read_last_commit_message( + policy.depth, policy.filter + ) if not msg: continue if self._verify_commit_msg_match_against_policy(msg, policy): diff --git a/src/releaser/hexagon/services/manifest_notifier.py b/src/releaser/hexagon/services/manifest_notifier.py index 7daea1b..f53815d 100644 --- a/src/releaser/hexagon/services/manifest_notifier.py +++ b/src/releaser/hexagon/services/manifest_notifier.py @@ -1,5 +1,6 @@ """Service used to send a POST request to a webhook URL with a manifest as JSON payload.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/src/releaser/infra/json_writer/stdout.py b/src/releaser/infra/json_writer/stdout.py index 7469d2c..8705bd6 100644 --- a/src/releaser/infra/json_writer/stdout.py +++ b/src/releaser/infra/json_writer/stdout.py @@ -8,7 +8,6 @@ class JsonStdoutWriter(JsonWriter): - """A JSON writer that writes to the standard output.""" def write_manifest(self, manifest: artefact.Manifest) -> None: From 31cc50cc42bdc933829e1b4f5a7275a377133322 Mon Sep 17 00:00:00 2001 From: Aurelien Pisu Date: Fri, 16 Feb 2024 10:54:56 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=20feat:=20add=20test=20to=20validate=20usa?= =?UTF-8?q?ge=20of=20branches=20=C2=B2on=20=20release=20strategy.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../hexagon/entities/strategy/policy.py | 4 +- .../hexagon/services/manifest_generator.py | 2 +- src/releaser/infra/git_reader/stub.py | 9 ++- tests/test_hexagon/test_manifest_generator.py | 77 +++++++++++++++---- 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index cf8a199..51ad3ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .devbox +.idea/ .venv/ __pycache__/ dist/ diff --git a/src/releaser/hexagon/entities/strategy/policy.py b/src/releaser/hexagon/entities/strategy/policy.py index b7e26d1..360a539 100644 --- a/src/releaser/hexagon/entities/strategy/policy.py +++ b/src/releaser/hexagon/entities/strategy/policy.py @@ -113,7 +113,7 @@ def parse_dict(cls, data: dict[str, Any]) -> "CommitMsgMatchPolicy": class Rule: """A rule for a release strategy.""" - branch: list[str] = field(default_factory=list) + branches: list[str] = field(default_factory=list) """The branches to match for the HEAD commit message.""" commit_msg: list[CommitMsgMatchPolicy] = field(default_factory=list) @@ -126,7 +126,7 @@ def parse_dict(cls, data: dict[str, Any]) -> Rule: """Parse a rule from a dictionary.""" return cls( - branch=_aslist(data.get("branch")), + branches=_aslist(data.get("branch")), commit_msg=[ CommitMsgMatchPolicy.parse_dict(policy) for policy in data.get("commit_msg", []) diff --git a/src/releaser/hexagon/services/manifest_generator.py b/src/releaser/hexagon/services/manifest_generator.py index f211c4a..92672b6 100644 --- a/src/releaser/hexagon/services/manifest_generator.py +++ b/src/releaser/hexagon/services/manifest_generator.py @@ -77,7 +77,7 @@ def _generate_application_tags( ) -> Iterator[str]: """Generate the list of tags to release for an application.""" for rule in application.on: - if not self.git_reader.current_reference_matches(rule.branch): + if not self.git_reader.current_reference_matches(rule.branches): continue for policy in rule.commit_msg: msg = self.git_reader.read_last_commit_message( diff --git a/src/releaser/infra/git_reader/stub.py b/src/releaser/infra/git_reader/stub.py index 1641964..65cb7b7 100644 --- a/src/releaser/infra/git_reader/stub.py +++ b/src/releaser/infra/git_reader/stub.py @@ -10,9 +10,12 @@ def __init__(self) -> None: self._sha: str | None = None self._history: list[str] | None = None self._is_dirty: bool | None = None + self._branch: str | None = None def read_current_branch(self) -> str: - return "testing" + if self._branch is None: + raise RuntimeError("branch not set in stub git reader") + return self._branch def is_dirty(self) -> bool: if self._is_dirty is None: @@ -40,3 +43,7 @@ def set_history(self, history: list[str]) -> None: def set_is_dirty(self, is_dirty: bool) -> None: """Test helper: Set the value that will be returned by is_dirty.""" self._is_dirty = is_dirty + + def set_branch(self, branch: str) -> None: + """ Test helper : set the value that be returned by read_current_branch.""" + self._branch = branch diff --git a/tests/test_hexagon/test_manifest_generator.py b/tests/test_hexagon/test_manifest_generator.py index 64a6a27..1b6372e 100644 --- a/tests/test_hexagon/test_manifest_generator.py +++ b/tests/test_hexagon/test_manifest_generator.py @@ -66,12 +66,70 @@ def test_without_image( assert manifest is not None assert manifest.applications == {} + @pytest.mark.parametrize( + "branches,output", + [ + ( + [], + artefact.Manifest( + applications={ + "test-app": artefact.Application( + images=[ + artefact.Image( + repository="test-image", + image="test-image:head", + tag="head", + platforms={}, + ) + ] + ) + } + ), + ), + (["something_else"], artefact.Manifest(applications={})), + ( + ["next"], + artefact.Manifest( + applications={ + "test-app": artefact.Application( + images=[ + artefact.Image( + repository="test-image", + image="test-image:head", + tag="head", + platforms={}, + ) + ] + ) + } + ), + ), + ( + ["something_else", "next"], + artefact.Manifest( + applications={ + "test-app": artefact.Application( + images=[ + artefact.Image( + repository="test-image", + image="test-image:head", + tag="head", + platforms={}, + ) + ] + ) + } + ), + ), + ], + ) def test_with_single_image_and_literal_commit_msg_matcher( - self, + self, branches: list[str], output: artefact.Manifest ): # Arrange self.git_reader.set_is_dirty(False) self.git_reader.set_history(["this is the latest commit message"]) + self.git_reader.set_branch("next") self.strategy_reader.set_strategy( strategy.ReleaseStrategy( applications={ @@ -79,12 +137,13 @@ def test_with_single_image_and_literal_commit_msg_matcher( images=[strategy.Image("test-image")], on=[ strategy.Rule( + branches=branches, commit_msg=[ strategy.CommitMsgMatchPolicy( match=["*"], tags=[strategy.LiteralTag(value="head")], ) - ] + ], ) ], ) @@ -96,17 +155,7 @@ def test_with_single_image_and_literal_commit_msg_matcher( # Assert manifest = self.json_writer.read_manifest() assert manifest is not None - assert manifest.applications == { - "test-app": artefact.Application( - images=[ - artefact.Image( - repository="test-image", - tag="head", - image="test-image:head", - ) - ] - ) - } + assert manifest == output @pytest.mark.parametrize( "last_commit", ["the first pattern is ...", "the second pattern is ..."] @@ -470,7 +519,7 @@ def test_with_single_image_and_git_sha_tag_policy_matcher_with_all_matcher( applications={ "test-app": strategy.Application( images=[strategy.Image("test-image")], - ) + ), }, on=[ strategy.Rule( From 9eda4012e7429c0d31fa6ca81a7b3e91ee1cac20 Mon Sep 17 00:00:00 2001 From: Aurelien Pisu Date: Fri, 16 Feb 2024 15:18:10 +0100 Subject: [PATCH 4/5] feat: add unit test tnhat vaiidatge strategy of quara-python, quara-frontend and quara-app. add unit-test that validate branches parameter strategy in app and by default --- .../entities/strategy/release_strategy.py | 10 +- .../manifest_quara-app-branch_next.json | 34 ++++++ .../manifest_quara-frontend-branch_next.json | 34 ++++++ .../manifest_quara-python-branch_next.json | 52 +++++++++ .../quara-app.package.json | 105 ++++++++++++++++++ .../quara-frontend.package.json | 90 +++++++++++++++ .../quara-python.pyproject.toml | 42 +++++++ .../test_cli/test_create_manifest_command.py | 86 +++++++++++++- 8 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json create mode 100644 tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json create mode 100644 tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json create mode 100644 tests/test_cli/regression_test_data/quara-app.package.json create mode 100644 tests/test_cli/regression_test_data/quara-frontend.package.json create mode 100644 tests/test_cli/regression_test_data/quara-python.pyproject.toml diff --git a/src/releaser/hexagon/entities/strategy/release_strategy.py b/src/releaser/hexagon/entities/strategy/release_strategy.py index c3dcb92..f3b43ec 100644 --- a/src/releaser/hexagon/entities/strategy/release_strategy.py +++ b/src/releaser/hexagon/entities/strategy/release_strategy.py @@ -23,13 +23,9 @@ class Application: @classmethod def parse_dict(cls, data: dict[str, Any]) -> "Application": """Parse an application from a dictionary.""" - policies: list[Any] | dict[str, Any] = data.get("on_commit_msg", []) - if isinstance(policies, list): - match_policies = [Rule.parse_dict(policy) for policy in policies] - else: - match_policies = [Rule.parse_dict(policies)] + on = [Rule.parse_dict(rule) for rule in data.get("on", {}).values()] return cls( - on=match_policies, + on=on, images=[_asimage(image) for image in data.get("images", [])], ) @@ -69,7 +65,7 @@ def parse_dict(cls, data: dict[str, Any]) -> "ReleaseStrategy": name: Application.parse_dict(application) for name, application in data.get("applications", {}).items() }, - on=[Rule.parse_dict(rule) for rule in data.get("on", [])], + on=[Rule.parse_dict(rule) for rule in data.get("on", {}).values()], ) def get_release_strategy_for_application( diff --git a/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json b/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json new file mode 100644 index 0000000..f97e1c6 --- /dev/null +++ b/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json @@ -0,0 +1,34 @@ +{ + "acquisition": { + "images": [ + { + "repository": "quara.azurecr.io/quara-acquisition-app", + "image": "quara.azurecr.io/quara-acquisition-app:next", + "tag": "next", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-acquisition-app", + "image": "quara.azurecr.io/quara-acquisition-app:shatest", + "tag": "shatest", + "platforms": {} + } + ] + }, + "operating-app": { + "images": [ + { + "repository": "quara.azurecr.io/quara-operator-app", + "image": "quara.azurecr.io/quara-operator-app:next", + "tag": "next", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-operator-app", + "image": "quara.azurecr.io/quara-operator-app:shatest", + "tag": "shatest", + "platforms": {} + } + ] + } +} \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json b/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json new file mode 100644 index 0000000..2138b95 --- /dev/null +++ b/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json @@ -0,0 +1,34 @@ +{ + "quara-frontend": { + "images": [ + { + "repository": "quara.azurecr.io/quara-frontend", + "image": "quara.azurecr.io/quara-frontend:shatest", + "tag": "shatest", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-frontend", + "image": "quara.azurecr.io/quara-frontend:next", + "tag": "next", + "platforms": {} + } + ] + }, + "quara-frontend-storybook": { + "images": [ + { + "repository": "quara.azurecr.io/quara-storybook", + "image": "quara.azurecr.io/quara-storybook:shatest", + "tag": "shatest", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-storybook", + "image": "quara.azurecr.io/quara-storybook:next", + "tag": "next", + "platforms": {} + } + ] + } +} \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json b/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json new file mode 100644 index 0000000..225da79 --- /dev/null +++ b/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json @@ -0,0 +1,52 @@ +{ + "quara-all-in-one": { + "images": [ + { + "repository": "quara.azurecr.io/quara-all-in-one", + "image": "quara.azurecr.io/quara-all-in-one:next", + "tag": "next", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-all-in-one", + "image": "quara.azurecr.io/quara-all-in-one:shatest", + "tag": "shatest", + "platforms": {} + } + ] + }, + "quara-ble-gateway": { + "images": [ + { + "repository": "quara.azurecr.io/quara-ble-gateway", + "image": "quara.azurecr.io/quara-ble-gateway:next", + "tag": "next", + "platforms": { + "linux/amd64": { + "image": "quara.azurecr.io/quara-ble-gateway:next-amd64", + "tag": "next-amd64" + }, + "linux/arm64": { + "image": "quara.azurecr.io/quara-ble-gateway:next-arm64", + "tag": "next-arm64" + } + } + }, + { + "repository": "quara.azurecr.io/quara-ble-gateway", + "image": "quara.azurecr.io/quara-ble-gateway:shatest", + "tag": "shatest", + "platforms": { + "linux/amd64": { + "image": "quara.azurecr.io/quara-ble-gateway:shatest-amd64", + "tag": "shatest-amd64" + }, + "linux/arm64": { + "image": "quara.azurecr.io/quara-ble-gateway:shatest-arm64", + "tag": "shatest-arm64" + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/quara-app.package.json b/tests/test_cli/regression_test_data/quara-app.package.json new file mode 100644 index 0000000..17b5edd --- /dev/null +++ b/tests/test_cli/regression_test_data/quara-app.package.json @@ -0,0 +1,105 @@ +{ + "quara": { + "schemas": { + "namespace": "quara", + "reference": "next", + "storageAccount": "devquacomapijqtcetlst0" + }, + "releaser": { + "on": { + "all": { + "commit_msg": [ + { + "match": "*", + "tags": [ + { + "type": "git_commit_sha", + "size": 7 + } + ] + } + ] + } + }, + "applications": { + "operating-app": { + "images": [ + "quara.azurecr.io/quara-operator-app" + ], + "on": { + "continuous-delivery": { + "branches": [ + "next" + ], + "commit_msg": [ + { + "match": "chore\\(release\\): [a-z-A-Z]* [0-9].[0-9]*.[0-9]", + "tags": [ + { + "value": "latest" + }, + { + "type": "version", + "file": "apps/operating-app/version.json" + } + ] + }, + { + "match": "*", + "tags": [ + { + "value": "next" + }, + { + "type": "git_commit_sha", + "size": 7 + } + ] + } + ] + } + } + }, + "acquisition": { + "images": [ + "quara.azurecr.io/quara-acquisition-app" + ], + "on": { + "all": {}, + "continuous-delivery": { + "branches": [ + "next" + ], + "commit_msg": [ + { + "match": "chore\\(release\\): [a-z-A-Z]* [0-9].[0-9]*.[0-9]", + "tags": [ + { + "value": "latest" + }, + { + "type": "version", + "file": "apps/acquisition/version.json" + } + ] + }, + { + "match": "*", + "tags": [ + { + "value": "next" + }, + { + "type": "git_commit_sha", + "size": 7 + } + ] + } + ] + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/quara-frontend.package.json b/tests/test_cli/regression_test_data/quara-frontend.package.json new file mode 100644 index 0000000..a2705f5 --- /dev/null +++ b/tests/test_cli/regression_test_data/quara-frontend.package.json @@ -0,0 +1,90 @@ +{ + "quara": { + "schemas": { + "namespace": "quara", + "reference": "next", + "storageAccount": "devquacomapijqtcetlst0" + }, + "releaser": { + "on": { + "all": { + "commit_msg": [ + { + "match": "*", + "tags": [ + { + "type": "git_commit_sha", + "size": 7 + } + ] + } + ] + }, + "continuous-delivery": { + "branches": [ + "next" + ], + "commit_msg": [ + { + "match": "*", + "tags": [ + { + "value": "next" + } + ] + }, + { + "match": [ + "chore\\(release\\): bumped to version [0-9]*.[0-9]*.[0-9]*\\-rc\\.[0-9]*\\s" + ], + "tags": [ + { + "value": "edge" + } + ] + }, + { + "match": [ + "chore\\(release\\): bumped to version [0-9]*.[0-9]*.[0-9]*", + "chore: merge from stable branch" + ], + "tags": [ + { + "type": "version" + } + ] + }, + { + "match": [ + "chore\\(release\\): bumped to version [0-9]*.[0-9]*.[0-9]*\\s" + ], + "tags": [ + { + "value": "latest" + } + ] + } + ] + } + }, + "applications": { + "quara-frontend": { + "images": [ + { + "repository": "quara.azurecr.io/quara-frontend", + "dockerfile": "docker/Dockerfile" + } + ] + }, + "quara-frontend-storybook": { + "images": [ + { + "repository": "quara.azurecr.io/quara-storybook", + "dockerfile": "docker/storybook/Dockerfile" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/quara-python.pyproject.toml b/tests/test_cli/regression_test_data/quara-python.pyproject.toml new file mode 100644 index 0000000..8fabc3d --- /dev/null +++ b/tests/test_cli/regression_test_data/quara-python.pyproject.toml @@ -0,0 +1,42 @@ + +[tool.quara.releaser.on.continuous-delivery] +branch = ["next"] +commit_msg = [ + { match = "*", tags = [ + { value = "next" } + ] }, + { match = [ + "chore\\(release\\): bump to version [0-9]*.[0-9]*.[0-9]*\\-rc\\.[0-9]*\\s", + ], tags = [ + { value = "edge" } + ] }, + { match = [ + "chore\\(release\\): bump to version [0-9]*.[0-9]*.[0-9]*", + "chore\\(release\\): merge from stable branch", + ], tags = [ + { type = "version" } + ] }, + { match = [ + "chore\\(release\\): bump to version [0-9]*.[0-9]*.[0-9]*\\s", + ], tags = [ + { value = "latest" } + ] }, +] + +[tool.quara.releaser.on.all] +commit_msg = [ + { match = "*", tags = [ + { type = "git_commit_sha", size = 7 } + ] } +] + +[tool.quara.releaser.applications.quara-all-in-one] +images = [{ repository = "quara.azurecr.io/quara-all-in-one" }] + +[tool.quara.releaser.applications.quara-ble-gateway] +images = [ + { repository = "quara.azurecr.io/quara-ble-gateway", platforms = [ + "linux/amd64", + "linux/arm64", + ] }, +] \ No newline at end of file diff --git a/tests/test_cli/test_create_manifest_command.py b/tests/test_cli/test_create_manifest_command.py index d9b17d3..c89ed86 100644 --- a/tests/test_cli/test_create_manifest_command.py +++ b/tests/test_cli/test_create_manifest_command.py @@ -1,7 +1,9 @@ from __future__ import annotations +import json import shlex - +from pathlib import Path +import toml import pytest from releaser.cli.app import Application @@ -33,12 +35,50 @@ def set_strategy(self, values: dict[str, object]) -> None: valid_strategy = strategy.ReleaseStrategy.parse_dict(values) self.deps.strategy_reader.set_strategy(valid_strategy) + def set_git_state( + self, + branch: str, + history: list[str], + sha: str | None = None, + dirty: bool = False, + ): + """a helper to set git state""" + self.deps.git_reader.set_history(history) + self.deps.git_reader.set_sha(sha) + self.deps.git_reader.set_branch(branch) + self.deps.git_reader.set_is_dirty(dirty) + def run_command(self, command: str) -> None: """A helper to run a command during test.""" self.app.execute(shlex.split(command)) + def read_pyproject_toml(self, file_name: str) -> dict[str, object]: + """a helper that read and return a toml object from file""" + return toml.loads(self.read_file("regression_test_data", file_name))["tool"][ + "quara" + ]["releaser"] + + @classmethod + def read_file(cls, *path: str) -> str: + """read file regarding path""" + path = Path(__file__).parent.joinpath(*path) + return path.read_text() + + def read_releaser_config(self, file_name: str, type: str) -> dict[str, object]: + if type == "toml": + return self.read_pyproject_toml(f"{file_name}.{type}") + elif type == "json": + return self.read_package_json(f"{file_name}.{type}") + + def read_package_json(self, file_name: str) -> dict[str, object]: + """read packages json for regression test""" + return json.loads(self.read_file("regression_test_data", file_name))["quara"][ + "releaser" + ] + class TestCreateManifestCommand(CreateManifestCommandSetup): + def test_it_should_create_an_empty_manifest(self): self.set_strategy( {"applications": {}}, @@ -51,3 +91,47 @@ def test_it_should_create_an_empty_manifest(self): "applications": {}, } ) + + @pytest.mark.parametrize( + "strategy_file_name, strategy_file_extension, branches, output_file_name", + [ + ( + "quara-frontend.package", + "json", + ["next"], + "manifest_quara-frontend-branch_next.json", + ), + ( + "quara-app.package", + "json", + ["next"], + "manifest_quara-app-branch_next.json", + ), + ( + "quara-python.pyproject", + "toml", + ["next"], + "manifest_quara-python-branch_next.json", + ), + ], + ) + def test_json_manifest( + self, + strategy_file_name: str, + strategy_file_extension: str, + branches: list[str], + output_file_name: str, + ): + self.set_strategy( + self.read_releaser_config(strategy_file_name, strategy_file_extension) + ) + self.set_git_state( + branch="next", history=["this is the latest commit message"], sha="shatest" + ) + self.run_command( + "create-manifest", + ) + manifest = json.loads( + self.read_file("regression_test_data", "output", output_file_name) + ) + self.assert_manifest({"applications": manifest}) From 0a0156a7e69b44d43ab3ecba2988f0cc3e6aafac Mon Sep 17 00:00:00 2001 From: Guillaume Charbonnier Date: Mon, 19 Feb 2024 14:09:26 +0100 Subject: [PATCH 5/5] minor changes after review --- .../manifest_quara-app-branch_next.json | 64 ++++++------ .../manifest_quara-frontend-branch_next.json | 64 ++++++------ .../manifest_quara-python-branch_next.json | 94 +++++++++--------- .../test_cli/test_create_manifest_command.py | 99 ++++++++++--------- 4 files changed, 168 insertions(+), 153 deletions(-) diff --git a/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json b/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json index f97e1c6..3b7c139 100644 --- a/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json +++ b/tests/test_cli/regression_test_data/output/manifest_quara-app-branch_next.json @@ -1,34 +1,36 @@ { - "acquisition": { - "images": [ - { - "repository": "quara.azurecr.io/quara-acquisition-app", - "image": "quara.azurecr.io/quara-acquisition-app:next", - "tag": "next", - "platforms": {} - }, - { - "repository": "quara.azurecr.io/quara-acquisition-app", - "image": "quara.azurecr.io/quara-acquisition-app:shatest", - "tag": "shatest", - "platforms": {} - } - ] - }, - "operating-app": { - "images": [ - { - "repository": "quara.azurecr.io/quara-operator-app", - "image": "quara.azurecr.io/quara-operator-app:next", - "tag": "next", - "platforms": {} - }, - { - "repository": "quara.azurecr.io/quara-operator-app", - "image": "quara.azurecr.io/quara-operator-app:shatest", - "tag": "shatest", - "platforms": {} - } - ] + "applications": { + "acquisition": { + "images": [ + { + "repository": "quara.azurecr.io/quara-acquisition-app", + "image": "quara.azurecr.io/quara-acquisition-app:next", + "tag": "next", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-acquisition-app", + "image": "quara.azurecr.io/quara-acquisition-app:shatest", + "tag": "shatest", + "platforms": {} + } + ] + }, + "operating-app": { + "images": [ + { + "repository": "quara.azurecr.io/quara-operator-app", + "image": "quara.azurecr.io/quara-operator-app:next", + "tag": "next", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-operator-app", + "image": "quara.azurecr.io/quara-operator-app:shatest", + "tag": "shatest", + "platforms": {} + } + ] + } } } \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json b/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json index 2138b95..5ff9824 100644 --- a/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json +++ b/tests/test_cli/regression_test_data/output/manifest_quara-frontend-branch_next.json @@ -1,34 +1,36 @@ { - "quara-frontend": { - "images": [ - { - "repository": "quara.azurecr.io/quara-frontend", - "image": "quara.azurecr.io/quara-frontend:shatest", - "tag": "shatest", - "platforms": {} - }, - { - "repository": "quara.azurecr.io/quara-frontend", - "image": "quara.azurecr.io/quara-frontend:next", - "tag": "next", - "platforms": {} - } - ] - }, - "quara-frontend-storybook": { - "images": [ - { - "repository": "quara.azurecr.io/quara-storybook", - "image": "quara.azurecr.io/quara-storybook:shatest", - "tag": "shatest", - "platforms": {} - }, - { - "repository": "quara.azurecr.io/quara-storybook", - "image": "quara.azurecr.io/quara-storybook:next", - "tag": "next", - "platforms": {} - } - ] + "applications": { + "quara-frontend": { + "images": [ + { + "repository": "quara.azurecr.io/quara-frontend", + "image": "quara.azurecr.io/quara-frontend:shatest", + "tag": "shatest", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-frontend", + "image": "quara.azurecr.io/quara-frontend:next", + "tag": "next", + "platforms": {} + } + ] + }, + "quara-frontend-storybook": { + "images": [ + { + "repository": "quara.azurecr.io/quara-storybook", + "image": "quara.azurecr.io/quara-storybook:shatest", + "tag": "shatest", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-storybook", + "image": "quara.azurecr.io/quara-storybook:next", + "tag": "next", + "platforms": {} + } + ] + } } } \ No newline at end of file diff --git a/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json b/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json index 225da79..7505b86 100644 --- a/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json +++ b/tests/test_cli/regression_test_data/output/manifest_quara-python-branch_next.json @@ -1,52 +1,54 @@ { - "quara-all-in-one": { - "images": [ - { - "repository": "quara.azurecr.io/quara-all-in-one", - "image": "quara.azurecr.io/quara-all-in-one:next", - "tag": "next", - "platforms": {} - }, - { - "repository": "quara.azurecr.io/quara-all-in-one", - "image": "quara.azurecr.io/quara-all-in-one:shatest", - "tag": "shatest", - "platforms": {} - } - ] - }, - "quara-ble-gateway": { - "images": [ - { - "repository": "quara.azurecr.io/quara-ble-gateway", - "image": "quara.azurecr.io/quara-ble-gateway:next", - "tag": "next", - "platforms": { - "linux/amd64": { - "image": "quara.azurecr.io/quara-ble-gateway:next-amd64", - "tag": "next-amd64" - }, - "linux/arm64": { - "image": "quara.azurecr.io/quara-ble-gateway:next-arm64", - "tag": "next-arm64" - } + "applications": { + "quara-all-in-one": { + "images": [ + { + "repository": "quara.azurecr.io/quara-all-in-one", + "image": "quara.azurecr.io/quara-all-in-one:next", + "tag": "next", + "platforms": {} + }, + { + "repository": "quara.azurecr.io/quara-all-in-one", + "image": "quara.azurecr.io/quara-all-in-one:shatest", + "tag": "shatest", + "platforms": {} } - }, - { - "repository": "quara.azurecr.io/quara-ble-gateway", - "image": "quara.azurecr.io/quara-ble-gateway:shatest", - "tag": "shatest", - "platforms": { - "linux/amd64": { - "image": "quara.azurecr.io/quara-ble-gateway:shatest-amd64", - "tag": "shatest-amd64" - }, - "linux/arm64": { - "image": "quara.azurecr.io/quara-ble-gateway:shatest-arm64", - "tag": "shatest-arm64" + ] + }, + "quara-ble-gateway": { + "images": [ + { + "repository": "quara.azurecr.io/quara-ble-gateway", + "image": "quara.azurecr.io/quara-ble-gateway:next", + "tag": "next", + "platforms": { + "linux/amd64": { + "image": "quara.azurecr.io/quara-ble-gateway:next-amd64", + "tag": "next-amd64" + }, + "linux/arm64": { + "image": "quara.azurecr.io/quara-ble-gateway:next-arm64", + "tag": "next-arm64" + } + } + }, + { + "repository": "quara.azurecr.io/quara-ble-gateway", + "image": "quara.azurecr.io/quara-ble-gateway:shatest", + "tag": "shatest", + "platforms": { + "linux/amd64": { + "image": "quara.azurecr.io/quara-ble-gateway:shatest-amd64", + "tag": "shatest-amd64" + }, + "linux/arm64": { + "image": "quara.azurecr.io/quara-ble-gateway:shatest-arm64", + "tag": "shatest-arm64" + } } } - } - ] + ] + } } } \ No newline at end of file diff --git a/tests/test_cli/test_create_manifest_command.py b/tests/test_cli/test_create_manifest_command.py index c89ed86..7711b81 100644 --- a/tests/test_cli/test_create_manifest_command.py +++ b/tests/test_cli/test_create_manifest_command.py @@ -3,8 +3,9 @@ import json import shlex from pathlib import Path -import toml + import pytest +import toml from releaser.cli.app import Application from releaser.hexagon.entities import artefact, strategy @@ -23,12 +24,17 @@ def setup( testing_dependencies=self.deps, ) + def read_manifest(self) -> artefact.Manifest: + """Get the generated manifest.""" + generated = self.deps.manifest_writer.read_manifest() + if generated is None: + raise ValueError("Manifest is not generated") + return generated + def assert_manifest(self, expected: dict[str, object]) -> None: - """An assertion helper to check the manifest contents.""" - assert ( - self.deps.manifest_writer.read_manifest() - == artefact.Manifest.parse_dict(expected) - ) + """Assert the generated manifest.""" + generated = self.read_manifest() + assert generated == generated.parse_dict(expected) def set_strategy(self, values: dict[str, object]) -> None: """A helper to set the release strategy during test.""" @@ -44,7 +50,8 @@ def set_git_state( ): """a helper to set git state""" self.deps.git_reader.set_history(history) - self.deps.git_reader.set_sha(sha) + if sha is not None: + self.deps.git_reader.set_sha(sha) self.deps.git_reader.set_branch(branch) self.deps.git_reader.set_is_dirty(dirty) @@ -52,29 +59,32 @@ def run_command(self, command: str) -> None: """A helper to run a command during test.""" self.app.execute(shlex.split(command)) - def read_pyproject_toml(self, file_name: str) -> dict[str, object]: - """a helper that read and return a toml object from file""" - return toml.loads(self.read_file("regression_test_data", file_name))["tool"][ - "quara" - ]["releaser"] + @staticmethod + def read_test_file(*path: str) -> str: + """read file regarding path""" + absolute_path = Path(__file__).parent.joinpath(*path) + return absolute_path.read_text() @classmethod - def read_file(cls, *path: str) -> str: - """read file regarding path""" - path = Path(__file__).parent.joinpath(*path) - return path.read_text() + def read_package_json(cls, file_name: str) -> dict[str, object]: + """read packages json for regression test""" + content = json.loads(cls.read_test_file("regression_test_data", file_name)) + return content["quara"]["releaser"] - def read_releaser_config(self, file_name: str, type: str) -> dict[str, object]: - if type == "toml": - return self.read_pyproject_toml(f"{file_name}.{type}") - elif type == "json": - return self.read_package_json(f"{file_name}.{type}") + @classmethod + def read_pyproject_toml(cls, file_name: str) -> dict[str, object]: + """a helper that read and return a toml object from file""" + content = toml.loads(cls.read_test_file("regression_test_data", file_name)) + return content["tool"]["quara"]["releaser"] - def read_package_json(self, file_name: str) -> dict[str, object]: - """read packages json for regression test""" - return json.loads(self.read_file("regression_test_data", file_name))["quara"][ - "releaser" - ] + @classmethod + def read_releaser_config(cls, file_name: str) -> dict[str, object]: + extension = file_name.split(".")[-1] + if extension == "toml": + return cls.read_pyproject_toml(file_name) + elif extension == "json": + return cls.read_package_json(file_name) + raise TypeError(f"Unknown file extension: {extension}") class TestCreateManifestCommand(CreateManifestCommandSetup): @@ -93,24 +103,21 @@ def test_it_should_create_an_empty_manifest(self): ) @pytest.mark.parametrize( - "strategy_file_name, strategy_file_extension, branches, output_file_name", + "strategy_file_name, branch, output_file_name", [ ( - "quara-frontend.package", - "json", - ["next"], + "quara-frontend.package.json", + "next", "manifest_quara-frontend-branch_next.json", ), ( - "quara-app.package", - "json", - ["next"], + "quara-app.package.json", + "next", "manifest_quara-app-branch_next.json", ), ( - "quara-python.pyproject", - "toml", - ["next"], + "quara-python.pyproject.toml", + "next", "manifest_quara-python-branch_next.json", ), ], @@ -118,20 +125,22 @@ def test_it_should_create_an_empty_manifest(self): def test_json_manifest( self, strategy_file_name: str, - strategy_file_extension: str, - branches: list[str], + branch: str, output_file_name: str, ): - self.set_strategy( - self.read_releaser_config(strategy_file_name, strategy_file_extension) + expected_output = self.read_test_file( + "regression_test_data", "output", output_file_name ) + # Arrange + self.set_strategy(self.read_releaser_config(strategy_file_name)) self.set_git_state( - branch="next", history=["this is the latest commit message"], sha="shatest" + branch=branch, + history=["this is the latest commit message"], + sha="shatest", ) + # Act self.run_command( "create-manifest", ) - manifest = json.loads( - self.read_file("regression_test_data", "output", output_file_name) - ) - self.assert_manifest({"applications": manifest}) + # Assert + self.assert_manifest(json.loads(expected_output))