RepoPlone 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. repoplone/__init__.py +17 -0
  2. repoplone/_types/__init__.py +43 -0
  3. repoplone/_types/package.py +40 -0
  4. repoplone/_types/pipeline.py +38 -0
  5. repoplone/_types/repository.py +134 -0
  6. repoplone/app.py +20 -0
  7. repoplone/cli.py +75 -0
  8. repoplone/commands/__init__.py +0 -0
  9. repoplone/commands/changelog.py +23 -0
  10. repoplone/commands/dependencies/__init__.py +211 -0
  11. repoplone/commands/release.py +64 -0
  12. repoplone/commands/settings.py +102 -0
  13. repoplone/commands/versions.py +72 -0
  14. repoplone/defaults.py +4 -0
  15. repoplone/distributions.py +53 -0
  16. repoplone/exceptions.py +13 -0
  17. repoplone/integrations/__init__.py +0 -0
  18. repoplone/integrations/base.py +46 -0
  19. repoplone/integrations/make.py +15 -0
  20. repoplone/integrations/pocompile.py +19 -0
  21. repoplone/integrations/release_it.py +42 -0
  22. repoplone/integrations/towncrier.py +50 -0
  23. repoplone/integrations/uv.py +60 -0
  24. repoplone/release/__init__.py +0 -0
  25. repoplone/release/_types.py +12 -0
  26. repoplone/release/config.py +143 -0
  27. repoplone/release/pipeline.py +59 -0
  28. repoplone/release/steps/__init__.py +62 -0
  29. repoplone/release/steps/backend.py +21 -0
  30. repoplone/release/steps/changelog.py +22 -0
  31. repoplone/release/steps/frontend.py +21 -0
  32. repoplone/release/steps/git.py +20 -0
  33. repoplone/release/steps/github.py +34 -0
  34. repoplone/release/steps/local_step.py +80 -0
  35. repoplone/release/steps/repository.py +41 -0
  36. repoplone/release/steps/summary.py +17 -0
  37. repoplone/release/steps/version.py +70 -0
  38. repoplone/settings/__init__.py +133 -0
  39. repoplone/settings/default.toml +55 -0
  40. repoplone/settings/parser.py +30 -0
  41. repoplone/utils/__init__.py +191 -0
  42. repoplone/utils/_git.py +78 -0
  43. repoplone/utils/_github.py +104 -0
  44. repoplone/utils/_hatch.py +23 -0
  45. repoplone/utils/_path.py +21 -0
  46. repoplone/utils/_requests.py +27 -0
  47. repoplone/utils/changelog.py +149 -0
  48. repoplone/utils/dependencies/__init__.py +51 -0
  49. repoplone/utils/dependencies/constraints.py +85 -0
  50. repoplone/utils/dependencies/frontend.py +158 -0
  51. repoplone/utils/dependencies/pyproject.py +246 -0
  52. repoplone/utils/dependencies/versions.py +126 -0
  53. repoplone/utils/display.py +72 -0
  54. repoplone/utils/python_release.py +37 -0
  55. repoplone/utils/release.py +125 -0
  56. repoplone/utils/settings.py +36 -0
  57. repoplone/utils/toml.py +26 -0
  58. repoplone/utils/versions/__init__.py +245 -0
  59. repoplone/utils/versions/calver.py +29 -0
  60. repoplone/utils/versions/semver.py +53 -0
  61. repoplone-1.0.0.dist-info/METADATA +491 -0
  62. repoplone-1.0.0.dist-info/RECORD +64 -0
  63. repoplone-1.0.0.dist-info/WHEEL +4 -0
  64. repoplone-1.0.0.dist-info/entry_points.txt +2 -0
repoplone/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+
4
+ __version__ = "1.0.0"
5
+
6
+
7
+ PACKAGE_NAME = "repoplone"
8
+
9
+
10
+ def _setup_logging():
11
+ logger = logging.getLogger(PACKAGE_NAME)
12
+ logger.addHandler(logging.StreamHandler())
13
+ logger.setLevel(logging.INFO)
14
+ return logger
15
+
16
+
17
+ logger = _setup_logging()
@@ -0,0 +1,43 @@
1
+ from .package import MrsDeveloperEntry
2
+ from .package import PackageConstraintInfo
3
+ from .package import Requirements
4
+ from .package import VersionChecker
5
+ from .package import VersionUpgrader
6
+ from .pipeline import PipelineReleaseStep
7
+ from .pipeline import PipelineReleaseStepFunction
8
+ from .pipeline import PipelineState
9
+ from .repository import BackendPackage
10
+ from .repository import Changelogs
11
+ from .repository import FrontendPackage
12
+ from .repository import Package
13
+ from .repository import RepositorySettings
14
+ from .repository import TowncrierSection
15
+ from .repository import TowncrierSettings
16
+ from dataclasses import dataclass
17
+
18
+
19
+ @dataclass
20
+ class CTLContextObject:
21
+ """Context object used by cli."""
22
+
23
+ settings: RepositorySettings
24
+
25
+
26
+ __all__ = [
27
+ "BackendPackage",
28
+ "CTLContextObject",
29
+ "Changelogs",
30
+ "FrontendPackage",
31
+ "MrsDeveloperEntry",
32
+ "Package",
33
+ "PackageConstraintInfo",
34
+ "PipelineReleaseStep",
35
+ "PipelineReleaseStepFunction",
36
+ "PipelineState",
37
+ "RepositorySettings",
38
+ "Requirements",
39
+ "TowncrierSection",
40
+ "TowncrierSettings",
41
+ "VersionChecker",
42
+ "VersionUpgrader",
43
+ ]
@@ -0,0 +1,40 @@
1
+ from .repository import RepositorySettings
2
+ from packaging.requirements import Requirement
3
+ from typing import NotRequired
4
+ from typing import Protocol
5
+ from typing import TypedDict
6
+
7
+
8
+ Requirements = dict[str, Requirement]
9
+
10
+
11
+ class PackageConstraintInfo(TypedDict):
12
+ """Definition on a Package constraint information."""
13
+
14
+ type: str
15
+ url: str
16
+ warning: NotRequired[str]
17
+
18
+
19
+ class VersionChecker(Protocol):
20
+ """Protocol for version checkers."""
21
+
22
+ def __call__(self, settings: RepositorySettings) -> tuple[str, str, str]: ...
23
+
24
+
25
+ class VersionUpgrader(Protocol):
26
+ """Protocol for version upgraders."""
27
+
28
+ def __call__(self, settings: RepositorySettings, version: str) -> bool: ...
29
+
30
+
31
+ class MrsDeveloperEntry(TypedDict):
32
+ """Definition of a mrs.developer.json entry."""
33
+
34
+ package: str
35
+ url: str
36
+ https: str
37
+ tag: str
38
+ output: NotRequired[str]
39
+ filterBlobs: bool
40
+ develop: bool
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from dataclasses import field
3
+ from repoplone._types.repository import RepositorySettings
4
+ from typing import Any
5
+ from typing import Protocol
6
+
7
+
8
+ @dataclass
9
+ class PipelineState:
10
+ """A run of the pipeline."""
11
+
12
+ dry_run: bool = False
13
+ version_format: str = "semver"
14
+ original_version: str = ""
15
+ next_version: str = ""
16
+ steps_current: int = 0
17
+ steps_total: int = 0
18
+
19
+
20
+ class PipelineReleaseStepFunction(Protocol):
21
+ def __call__(
22
+ self,
23
+ step_id: str,
24
+ title: str,
25
+ settings: RepositorySettings,
26
+ state: PipelineState,
27
+ **kwargs: Any,
28
+ ) -> bool: ...
29
+
30
+
31
+ @dataclass
32
+ class PipelineReleaseStep:
33
+ """Definition of a release step."""
34
+
35
+ id: str
36
+ title: str
37
+ func: PipelineReleaseStepFunction
38
+ kwargs: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from dataclasses import field
5
+ from packaging.requirements import Requirement
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from repoplone._types.pipeline import PipelineReleaseStep
12
+
13
+
14
+ Requirements = dict[str, Requirement]
15
+
16
+
17
+ @dataclass
18
+ class Changelogs:
19
+ """Changelog locations."""
20
+
21
+ root: Path
22
+ backend: Path
23
+ frontend: Path
24
+
25
+ def sanity(self) -> bool:
26
+ return self.root.exists()
27
+
28
+
29
+ @dataclass
30
+ class Package:
31
+ """Package information."""
32
+
33
+ enabled: bool
34
+ name: str
35
+ path: Path
36
+ changelog: Path
37
+ towncrier: Path
38
+ base_package: str
39
+ code_path: Path
40
+ base_package_version: str = ""
41
+ publish: bool = True
42
+ version: str = ""
43
+
44
+ def sanity(self) -> bool:
45
+ if not self.enabled:
46
+ return True
47
+ return (
48
+ self.path.exists() and self.changelog.exists() and self.towncrier.exists()
49
+ )
50
+
51
+
52
+ @dataclass
53
+ class BackendPackage(Package):
54
+ """Backend package information."""
55
+
56
+ managed_by_uv: bool = False
57
+ python_version: str = ""
58
+ python_versions: list[str] = field(default_factory=list)
59
+ plone_versions: list[str] = field(default_factory=list)
60
+
61
+
62
+ @dataclass
63
+ class FrontendPackage(Package):
64
+ """Frontend package information."""
65
+
66
+ volto_version: str = ""
67
+
68
+
69
+ @dataclass
70
+ class TowncrierSection:
71
+ """Towncrier section."""
72
+
73
+ section_id: str
74
+ name: str
75
+ path: Path
76
+
77
+ def sanity(self) -> bool:
78
+ return self.path.exists() if self.path else False
79
+
80
+
81
+ @dataclass
82
+ class TowncrierSettings:
83
+ """Towncrier settings."""
84
+
85
+ sections: list[TowncrierSection]
86
+
87
+ def __getattr__(self, name: str):
88
+ for section in self.sections:
89
+ if section.section_id == name:
90
+ return section
91
+ raise AttributeError(f"{name} not found")
92
+
93
+ def sanity(self) -> bool:
94
+ sections = self.sections
95
+ checks = [section.sanity() for section in sections]
96
+ return all(checks)
97
+
98
+
99
+ @dataclass
100
+ class RepositorySettings:
101
+ """Settings for a distribution."""
102
+
103
+ name: str
104
+ managed_by_uv: bool
105
+ root_path: Path
106
+ version: str
107
+ version_format: str
108
+ container_images_prefix: str
109
+ backend: BackendPackage
110
+ frontend: FrontendPackage
111
+ version_path: Path
112
+ compose_path: list[Path]
113
+ towncrier: TowncrierSettings
114
+ changelogs: Changelogs
115
+ release_steps: list[PipelineReleaseStep] = field(default_factory=list)
116
+ remote_origin: str = ""
117
+ issues_url: str = ""
118
+ _tmp_changelog: str = ""
119
+
120
+ @property
121
+ def path(self) -> Path:
122
+ return self.root_path
123
+
124
+ def sanity(self) -> bool:
125
+ steps = [
126
+ self.root_path.exists(),
127
+ self.backend.sanity(),
128
+ self.frontend.sanity(),
129
+ self.version_path.exists(),
130
+ all(path.exists() for path in self.compose_path),
131
+ self.towncrier.sanity(),
132
+ self.changelogs.sanity(),
133
+ ]
134
+ return all(steps)
repoplone/app.py ADDED
@@ -0,0 +1,20 @@
1
+ from repoplone.exceptions import RepoPloneException
2
+
3
+ import sys
4
+ import typer
5
+
6
+
7
+ class RepoPlone(typer.Typer):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args, **kwargs)
10
+
11
+ def __call__(self, *args, **kwargs):
12
+ try:
13
+ super().__call__(*args, **kwargs)
14
+ except typer.Exit as exc:
15
+ sys.exit(exc.exit_code)
16
+ except RepoPloneException as exc:
17
+ typer.echo(f"Error: {exc.message}", err=True)
18
+ sys.exit(1)
19
+ except Exception as exc:
20
+ raise exc
repoplone/cli.py ADDED
@@ -0,0 +1,75 @@
1
+ from repoplone import __version__
2
+ from repoplone import _types as t
3
+ from repoplone.app import RepoPlone
4
+ from repoplone.commands.changelog import app as app_changelog
5
+ from repoplone.commands.dependencies import app as app_deps
6
+ from repoplone.commands.release import app as app_release
7
+ from repoplone.commands.settings import app as app_settings
8
+ from repoplone.commands.versions import app as app_versions
9
+ from repoplone.settings import get_settings
10
+ from typing import Annotated
11
+
12
+ import typer
13
+
14
+
15
+ app = RepoPlone(no_args_is_help=True)
16
+
17
+
18
+ @app.callback(invoke_without_command=True)
19
+ def main(
20
+ ctx: typer.Context,
21
+ version: Annotated[
22
+ bool, typer.Option(help="Report the version of this app.")
23
+ ] = False,
24
+ ):
25
+ """Welcome to Plone Repository Helper."""
26
+ if version:
27
+ typer.echo(f"repoplone {__version__}")
28
+ return
29
+ try:
30
+ settings = get_settings()
31
+ except RuntimeError:
32
+ typer.echo("Did not find a repository.toml file.")
33
+ raise typer.Exit() from None
34
+ ctx_obj = t.CTLContextObject(settings=settings)
35
+ ctx.obj = ctx_obj
36
+ ctx.ensure_object(t.CTLContextObject)
37
+
38
+
39
+ app.add_typer(
40
+ app_changelog,
41
+ name="changelog",
42
+ no_args_is_help=False,
43
+ help="Displays a draft of Change log entries",
44
+ )
45
+ app.add_typer(
46
+ app_release,
47
+ name="release",
48
+ no_args_is_help=False,
49
+ help="Release packages in this repository",
50
+ )
51
+ app.add_typer(
52
+ app_deps,
53
+ name="deps",
54
+ no_args_is_help=True,
55
+ help="Check and manage dependencies",
56
+ )
57
+ app.add_typer(
58
+ app_settings,
59
+ name="settings",
60
+ no_args_is_help=True,
61
+ help="Manage settings for a repository",
62
+ )
63
+ app.add_typer(
64
+ app_versions,
65
+ name="versions",
66
+ no_args_is_help=True,
67
+ help="Display version information about this repository",
68
+ )
69
+
70
+
71
+ def cli():
72
+ app()
73
+
74
+
75
+ __all__ = ["cli"]
File without changes
@@ -0,0 +1,23 @@
1
+ from repoplone import _types as t
2
+ from repoplone.app import RepoPlone
3
+ from repoplone.utils import changelog as chgutils
4
+ from repoplone.utils import display as dutils
5
+
6
+ import typer
7
+
8
+
9
+ app = RepoPlone()
10
+
11
+
12
+ @app.callback(invoke_without_command=True)
13
+ def main(
14
+ ctx: typer.Context,
15
+ ):
16
+ """Generate a draft of the final changelog."""
17
+ settings: t.RepositorySettings = ctx.obj.settings
18
+ original_version = settings.version
19
+ # Changelog
20
+ new_entries, _ = chgutils.update_changelog(
21
+ settings, draft=True, version=original_version
22
+ )
23
+ dutils.print(f"{'=' * 50}\n{new_entries}\n{'=' * 50}")
@@ -0,0 +1,211 @@
1
+ from repoplone import _types as t
2
+ from repoplone import logger
3
+ from repoplone import utils
4
+ from repoplone.app import RepoPlone
5
+ from repoplone.integrations.make import Make
6
+ from repoplone.utils import dependencies
7
+ from repoplone.utils import display as dutils
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+
13
+ app = RepoPlone()
14
+
15
+
16
+ @app.command()
17
+ def info(ctx: typer.Context):
18
+ """Report the base packages in use."""
19
+ settings: t.RepositorySettings = ctx.obj.settings
20
+ title = "Base packages"
21
+ cols = [
22
+ {"header": "Component"},
23
+ {"header": "Package Name"},
24
+ ]
25
+ rows = []
26
+ if settings.backend.enabled:
27
+ rows.append(["Backend", settings.backend.base_package])
28
+ if settings.frontend.enabled:
29
+ rows.append(["Frontend", settings.frontend.base_package])
30
+ table = dutils.table(title, cols, rows)
31
+ dutils.print(table)
32
+
33
+
34
+ @app.command()
35
+ def check(ctx: typer.Context):
36
+ """Check latest version of base package and compare it to our current pinning."""
37
+ settings: t.RepositorySettings = ctx.obj.settings
38
+ title = "Base packages versions"
39
+ cols = [
40
+ {"header": "Component"},
41
+ {"header": "Package Name"},
42
+ {"header": "Current Version"},
43
+ {"header": "Latest Version"},
44
+ ]
45
+ rows = []
46
+ if settings.backend.enabled:
47
+ backend_versions = dependencies.check_backend_base_package(settings)
48
+ rows.append(["Backend", *backend_versions])
49
+ if settings.frontend.enabled:
50
+ frontend_versions = dependencies.check_frontend_base_package(settings)
51
+ rows.append(["Frontend", *frontend_versions])
52
+ table = dutils.table(title, cols, rows)
53
+ dutils.print(table)
54
+
55
+
56
+ def _upgrade_backend(settings: t.RepositorySettings, version: str) -> bool:
57
+ """Upgrade a base dependency to a newer version."""
58
+ package_name: str = settings.backend.base_package
59
+ logger.info(f"Getting {package_name} constraints for version {version}")
60
+ pyproject_path = utils.get_pyproject(settings)
61
+ if pyproject_path:
62
+ status = dependencies.update_backend_constraints(
63
+ settings, pyproject_path, package_name, version
64
+ )
65
+ # Update versions.txt if it exists
66
+ backend_path = settings.backend.path
67
+ version_file = (backend_path / "version.txt").resolve()
68
+ if status and version_file.exists():
69
+ version_file.write_text(f"{version}\n")
70
+ return status
71
+ else:
72
+ return False
73
+
74
+
75
+ def _upgrade_frontend(settings: t.RepositorySettings, version: str) -> bool:
76
+ """Upgrade a base dependency to a newer version."""
77
+ package_name: str = settings.frontend.base_package
78
+ return dependencies.update_frontend_base_package(settings, package_name, version)
79
+
80
+
81
+ def _sync_dependencies(settings: t.RepositorySettings, component: str):
82
+ """Sync the lockfile for a specific component."""
83
+ path = settings.backend.path if component == "backend" else settings.frontend.path
84
+ towncrier_path = path / "news"
85
+ make = Make(settings.root_path)
86
+ try:
87
+ target = f"{component}-install"
88
+ typer.echo(f" - Will update lockfile by running 'make {target}'")
89
+ make.run(target)
90
+ except RuntimeError as e:
91
+ typer.echo(f" - Error running 'make {target}: {e}")
92
+ raise typer.Exit(1) from e
93
+ typer.echo("\n")
94
+ typer.echo(f"Now, please, add a news entry in {towncrier_path}.")
95
+
96
+
97
+ @app.command()
98
+ def constraints(
99
+ ctx: typer.Context,
100
+ component: Annotated[
101
+ str,
102
+ typer.Argument(help="Which component to update constraints?"),
103
+ ] = "backend",
104
+ ):
105
+ """Update constraints for a specific component."""
106
+ settings: t.RepositorySettings = ctx.obj.settings
107
+ if component not in ("backend"):
108
+ typer.echo("Component must be 'backend'.")
109
+ raise typer.Exit(1)
110
+ managed_by_uv = settings.backend.managed_by_uv
111
+ pyproject_path = utils.get_pyproject(settings)
112
+ if managed_by_uv and pyproject_path:
113
+ info = dependencies.check_backend_base_package(settings)
114
+ package_name, version = info[0:2]
115
+ dependencies.update_backend_constraints(
116
+ settings, pyproject_path, package_name, version
117
+ )
118
+ typer.echo("Updated constraints in pyproject.toml.")
119
+ else:
120
+ typer.echo("Only available in projects managed by uv (pyproject.toml).")
121
+
122
+
123
+ @app.command()
124
+ def sync(
125
+ ctx: typer.Context,
126
+ component: Annotated[
127
+ str,
128
+ typer.Argument(
129
+ help="Which component to sync the lockfile? backend or frontend"
130
+ ),
131
+ ] = "both",
132
+ ):
133
+ """Sync the lockfile for a specific component."""
134
+ settings: t.RepositorySettings = ctx.obj.settings
135
+ if component not in ("backend", "frontend", "both"):
136
+ typer.echo("Component must be either 'backend' or 'frontend'.")
137
+ raise typer.Exit(1)
138
+
139
+ components = []
140
+ if component == "both":
141
+ if settings.backend.enabled:
142
+ components.append("backend")
143
+ if settings.frontend.enabled:
144
+ components.append("frontend")
145
+ else:
146
+ # Check if component is enabled
147
+ section = getattr(settings, component)
148
+ if not section.enabled:
149
+ typer.echo(
150
+ f"Error: {component.title()} component is not enabled "
151
+ "in repository.toml"
152
+ )
153
+ raise typer.Exit(1)
154
+ components = [component]
155
+
156
+ for component_ in components:
157
+ _sync_dependencies(settings, component_)
158
+ typer.echo("\n")
159
+
160
+
161
+ UPGRADE_FUNC: dict[str, tuple[t.VersionChecker, t.VersionUpgrader]] = {
162
+ "backend": (dependencies.check_backend_base_package, _upgrade_backend),
163
+ "frontend": (dependencies.check_frontend_base_package, _upgrade_frontend),
164
+ }
165
+
166
+
167
+ @app.command()
168
+ def upgrade(
169
+ ctx: typer.Context,
170
+ component: Annotated[
171
+ str, typer.Argument(help="Which component to upgrade? backend or frontend?")
172
+ ],
173
+ version: Annotated[
174
+ str, typer.Argument(help="New version the base package")
175
+ ] = "latest",
176
+ ):
177
+ """Upgrade a base dependency to a newer version."""
178
+ settings: t.RepositorySettings = ctx.obj.settings
179
+ if component not in ("backend", "frontend"):
180
+ typer.echo("Component must be either 'backend' or 'frontend'.")
181
+ raise typer.Exit(1)
182
+
183
+ # Check if component is enabled
184
+ section = getattr(settings, component)
185
+ if not section.enabled:
186
+ typer.echo(
187
+ f"Error: {component.title()} component is not enabled in repository.toml"
188
+ )
189
+ raise typer.Exit(1)
190
+
191
+ info_func, upgrade_func = UPGRADE_FUNC[component]
192
+
193
+ try:
194
+ info = info_func(settings)
195
+ except RuntimeError as e:
196
+ typer.echo(f" - Error checking {component} base package: {e}")
197
+ raise typer.Exit(1) from e
198
+ package_name, current_version = info[0:2]
199
+ if version == "latest":
200
+ version = info[2] # Latest version
201
+ package_title = f"{package_name} ({component.title()})"
202
+ if version != current_version:
203
+ typer.echo(f"Upgrade {package_title} from {current_version} to {version}")
204
+ if upgrade_func(settings, version):
205
+ typer.echo(f"- {package_title} at version {version}.")
206
+ _sync_dependencies(settings, component)
207
+ else:
208
+ typer.echo(f"- Failed to upgrade {package_title} to version {version}.")
209
+ raise typer.Exit(1)
210
+ else:
211
+ typer.echo(f"{package_title} already at version {version}.")
@@ -0,0 +1,64 @@
1
+ from repoplone import _types as t
2
+ from repoplone.app import RepoPlone
3
+ from repoplone.release.pipeline import ReleasePipeline
4
+ from repoplone.utils import display as dutils
5
+ from repoplone.utils import release as utils
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+
11
+ app = RepoPlone()
12
+
13
+
14
+ NO_VERSION: str = "next"
15
+
16
+
17
+ def _preflight_check(settings: t.RepositorySettings) -> bool:
18
+ """Check if the repository is ready for a release."""
19
+ status: bool = True
20
+ sanity = utils.sanity_check(settings)
21
+
22
+ if sanity.warnings:
23
+ dutils.print("\n[bold yellow]Warnings:[/bold yellow]")
24
+ for warning in sanity.warnings:
25
+ dutils.print(f"- [yellow]{warning}[/yellow]")
26
+
27
+ if sanity.errors:
28
+ dutils.print("\n[bold red]Errors:[/bold red]")
29
+ for error in sanity.errors:
30
+ dutils.print(f"- [red]{error}[/red]")
31
+ raise typer.Exit(1)
32
+
33
+ if sanity.warnings:
34
+ status = dutils.check_confirm("Do you want to continue the release?")
35
+ return status
36
+
37
+
38
+ @app.callback(invoke_without_command=True)
39
+ def main(
40
+ ctx: typer.Context,
41
+ desired_version: Annotated[
42
+ str,
43
+ typer.Argument(
44
+ help=(
45
+ "Next version. Could be the version number, or "
46
+ "a segment like: a, minor, major, rc"
47
+ ),
48
+ ),
49
+ ] = NO_VERSION,
50
+ dry_run: Annotated[bool, typer.Option(help="Is this a dry run?")] = False,
51
+ ):
52
+ """Release the packages in this repository."""
53
+ settings: t.RepositorySettings = ctx.obj.settings
54
+ dutils.print(f"\n[bold green]Release {settings.name}[/bold green]")
55
+ if not _preflight_check(settings):
56
+ raise typer.Exit(0)
57
+ pipeline = ReleasePipeline(settings, dry_run, desired_version)
58
+ try:
59
+ pipeline()
60
+ except RuntimeError as e:
61
+ dutils.print(f"\n[bold red]Release failed: {e}[/bold red]")
62
+ raise typer.Exit(1) from e
63
+
64
+ raise typer.Exit(0)