Coverage for src/ezcompiler/services/release_service.py: 85.00%
32 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
1# ///////////////////////////////////////////////////////////////
2# RELEASE_SERVICE - Secure-release orchestration service
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Release service - Orchestrates secure-release packaging.
9Builds a signed TUF repository locally via a releaser adapter, then OPTIONALLY
10delegates the remote transfer of that repository tree to the existing
11``UploaderService`` (disk/server). Release packaging and transfer stay
12separate concerns.
13"""
15from __future__ import annotations
17# ///////////////////////////////////////////////////////////////
18# IMPORTS
19# ///////////////////////////////////////////////////////////////
20from pathlib import Path
21from typing import TYPE_CHECKING, Any, Literal, cast
23from ..adapters import ReleaserFactory
24from ..shared.exceptions import ReleaseError
25from .uploader_service import UploaderService
27if TYPE_CHECKING: 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true
28 from .._types import ReleaserPort
30# ///////////////////////////////////////////////////////////////
31# CLASSES
32# ///////////////////////////////////////////////////////////////
35class ReleaseService:
36 """Service orchestrating secure-release packaging and publication."""
38 # ------------------------------------------------
39 # RELEASE METHODS
40 # ------------------------------------------------
42 @staticmethod
43 def release_and_publish(
44 bundle_dir: Path,
45 app_name: str,
46 version: str,
47 repo_dir: Path,
48 *,
49 release_type: str = "tufup",
50 publish: bool = False,
51 pull_before: bool = False,
52 upload_type: str | None = None,
53 destination: str | None = None,
54 releaser_config: dict[str, Any] | None = None,
55 upload_config: dict[str, Any] | None = None,
56 ) -> Path:
57 """Build the local TUF repo, then optionally publish it.
59 Args:
60 bundle_dir: Directory containing the compiled application.
61 app_name: Application name (used by tufup to name bundles).
62 version: Application version string.
63 repo_dir: Root directory for the local TUF repository tree.
64 release_type: Release backend to use (default: "tufup").
65 publish: When True, transfer the repository/ tree via an uploader.
66 pull_before: When True, download the current remote tree into
67 ``repo_dir`` before releasing (R2 source-of-truth cycle).
68 Requires upload_type and destination.
69 upload_type: Upload backend ("disk" or "server"). Required when publish=True.
70 destination: Upload destination path or URL. Required when publish=True.
71 releaser_config: Extra config forwarded to the releaser adapter.
72 upload_config: Extra config forwarded to the uploader adapter.
74 Returns:
75 Path: The local ``repository/`` tree path.
77 Raises:
78 ValueError: When publish=True but upload_type or destination is missing.
79 ReleaseError: When release packaging or publishing fails.
80 """
81 if pull_before and upload_type and destination:
82 UploaderService.download(
83 remote_source=destination,
84 upload_type=cast(Literal["disk", "server", "r2"], upload_type),
85 destination_local=repo_dir,
86 upload_config=upload_config,
87 )
89 releaser: ReleaserPort = ReleaserFactory.create_releaser(
90 release_type, releaser_config
91 )
92 repository_path = releaser.release(
93 bundle_dir=bundle_dir,
94 app_name=app_name,
95 version=version,
96 repo_dir=repo_dir,
97 )
99 if not publish:
100 return repository_path
102 if not upload_type or not destination:
103 raise ValueError("publish=True requires both upload_type and destination")
105 try:
106 UploaderService.upload(
107 source_path=repository_path,
108 upload_type=cast(Literal["disk", "server", "r2"], upload_type),
109 destination=destination,
110 upload_config=upload_config,
111 )
112 except Exception as exc:
113 raise ReleaseError(f"Publishing release repository failed: {exc}") from exc
115 return repository_path
117 @staticmethod
118 def init_release(
119 app_name: str,
120 repo_dir: Path,
121 keys_dir: Path,
122 *,
123 release_type: str = "tufup",
124 releaser_config: dict[str, Any] | None = None,
125 ) -> bool:
126 """Crée le releaser via la factory et délègue à init_keys.
128 Returns True si init effectuée, False si déjà présente (skip).
129 """
130 releaser: ReleaserPort = ReleaserFactory.create_releaser(
131 release_type, releaser_config
132 )
133 return releaser.init_keys(
134 app_name=app_name, repo_dir=repo_dir, keys_dir=keys_dir
135 )
137 @staticmethod
138 def refresh_expiration(
139 app_name: str,
140 repo_dir: Path,
141 keys_dir: Path,
142 *,
143 roles: tuple[str, ...] = ("targets", "snapshot", "timestamp"),
144 days: int | None = None,
145 release_type: str = "tufup",
146 releaser_config: dict[str, Any] | None = None,
147 ) -> Path:
148 """Re-signe les metadata pour repousser l'expiration sans nouvelle release.
150 Keep-alive natif tufup pour les projets mis à jour irrégulièrement.
152 Returns le dossier du repo TUF local.
153 """
154 releaser: ReleaserPort = ReleaserFactory.create_releaser(
155 release_type, releaser_config
156 )
157 return releaser.refresh_expiration(
158 app_name=app_name,
159 repo_dir=repo_dir,
160 keys_dir=keys_dir,
161 roles=roles,
162 days=days,
163 )