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

1# /////////////////////////////////////////////////////////////// 

2# RELEASE_SERVICE - Secure-release orchestration service 

3# Project: ezcompiler 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7Release service - Orchestrates secure-release packaging. 

8 

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""" 

14 

15from __future__ import annotations 

16 

17# /////////////////////////////////////////////////////////////// 

18# IMPORTS 

19# /////////////////////////////////////////////////////////////// 

20from pathlib import Path 

21from typing import TYPE_CHECKING, Any, Literal, cast 

22 

23from ..adapters import ReleaserFactory 

24from ..shared.exceptions import ReleaseError 

25from .uploader_service import UploaderService 

26 

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 

29 

30# /////////////////////////////////////////////////////////////// 

31# CLASSES 

32# /////////////////////////////////////////////////////////////// 

33 

34 

35class ReleaseService: 

36 """Service orchestrating secure-release packaging and publication.""" 

37 

38 # ------------------------------------------------ 

39 # RELEASE METHODS 

40 # ------------------------------------------------ 

41 

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. 

58 

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. 

73 

74 Returns: 

75 Path: The local ``repository/`` tree path. 

76 

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 ) 

88 

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 ) 

98 

99 if not publish: 

100 return repository_path 

101 

102 if not upload_type or not destination: 

103 raise ValueError("publish=True requires both upload_type and destination") 

104 

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 

114 

115 return repository_path 

116 

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. 

127 

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 ) 

136 

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. 

149 

150 Keep-alive natif tufup pour les projets mis à jour irrégulièrement. 

151 

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 )