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.
- repoplone/__init__.py +17 -0
- repoplone/_types/__init__.py +43 -0
- repoplone/_types/package.py +40 -0
- repoplone/_types/pipeline.py +38 -0
- repoplone/_types/repository.py +134 -0
- repoplone/app.py +20 -0
- repoplone/cli.py +75 -0
- repoplone/commands/__init__.py +0 -0
- repoplone/commands/changelog.py +23 -0
- repoplone/commands/dependencies/__init__.py +211 -0
- repoplone/commands/release.py +64 -0
- repoplone/commands/settings.py +102 -0
- repoplone/commands/versions.py +72 -0
- repoplone/defaults.py +4 -0
- repoplone/distributions.py +53 -0
- repoplone/exceptions.py +13 -0
- repoplone/integrations/__init__.py +0 -0
- repoplone/integrations/base.py +46 -0
- repoplone/integrations/make.py +15 -0
- repoplone/integrations/pocompile.py +19 -0
- repoplone/integrations/release_it.py +42 -0
- repoplone/integrations/towncrier.py +50 -0
- repoplone/integrations/uv.py +60 -0
- repoplone/release/__init__.py +0 -0
- repoplone/release/_types.py +12 -0
- repoplone/release/config.py +143 -0
- repoplone/release/pipeline.py +59 -0
- repoplone/release/steps/__init__.py +62 -0
- repoplone/release/steps/backend.py +21 -0
- repoplone/release/steps/changelog.py +22 -0
- repoplone/release/steps/frontend.py +21 -0
- repoplone/release/steps/git.py +20 -0
- repoplone/release/steps/github.py +34 -0
- repoplone/release/steps/local_step.py +80 -0
- repoplone/release/steps/repository.py +41 -0
- repoplone/release/steps/summary.py +17 -0
- repoplone/release/steps/version.py +70 -0
- repoplone/settings/__init__.py +133 -0
- repoplone/settings/default.toml +55 -0
- repoplone/settings/parser.py +30 -0
- repoplone/utils/__init__.py +191 -0
- repoplone/utils/_git.py +78 -0
- repoplone/utils/_github.py +104 -0
- repoplone/utils/_hatch.py +23 -0
- repoplone/utils/_path.py +21 -0
- repoplone/utils/_requests.py +27 -0
- repoplone/utils/changelog.py +149 -0
- repoplone/utils/dependencies/__init__.py +51 -0
- repoplone/utils/dependencies/constraints.py +85 -0
- repoplone/utils/dependencies/frontend.py +158 -0
- repoplone/utils/dependencies/pyproject.py +246 -0
- repoplone/utils/dependencies/versions.py +126 -0
- repoplone/utils/display.py +72 -0
- repoplone/utils/python_release.py +37 -0
- repoplone/utils/release.py +125 -0
- repoplone/utils/settings.py +36 -0
- repoplone/utils/toml.py +26 -0
- repoplone/utils/versions/__init__.py +245 -0
- repoplone/utils/versions/calver.py +29 -0
- repoplone/utils/versions/semver.py +53 -0
- repoplone-1.0.0.dist-info/METADATA +491 -0
- repoplone-1.0.0.dist-info/RECORD +64 -0
- repoplone-1.0.0.dist-info/WHEEL +4 -0
- 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)
|