@@ -12,6 +12,8 @@ class SharedDeployArtifacts:
1212 base_path : str | None = None
1313 managed_dirs : list [str ] = field (default_factory = list )
1414 managed_files : list [str ] = field (default_factory = list )
15+ managed_recursive_dirs : list [str ] = field (default_factory = list )
16+ root_group_writable_dirs : list [str ] = field (default_factory = list )
1517
1618
1719def create_shared_deploy_artifacts (
@@ -29,11 +31,15 @@ def create_shared_deploy_artifacts(
2931 repo_root = repo_root ,
3032 field_name = 'shared.base_path' ,
3133 )
32- managed_dirs = _normalize_path_entries (
34+ (
35+ managed_recursive_dirs ,
36+ root_group_writable_dirs ,
37+ ) = _normalize_managed_directory_entries (
3338 shared_cfg .get ('managed_directories' ),
3439 repo_root = repo_root ,
3540 field_name = 'shared.managed_directories' ,
3641 )
42+ managed_dirs : list [str ] = []
3743 managed_files = _normalize_path_entries (
3844 shared_cfg .get ('managed_files' ),
3945 repo_root = repo_root ,
@@ -81,10 +87,16 @@ def create_shared_deploy_artifacts(
8187 dest_link .symlink_to (target_path )
8288 managed_dirs .append (str (dest_link .parent ))
8389
90+ managed_dirs = _dedupe_existing_paths (managed_dirs )
91+
8492 return SharedDeployArtifacts (
8593 base_path = base_path ,
86- managed_dirs = _dedupe_existing_paths ( managed_dirs ) ,
94+ managed_dirs = managed_dirs ,
8795 managed_files = _dedupe_existing_paths (managed_files ),
96+ managed_recursive_dirs = _dedupe_existing_paths (managed_recursive_dirs ),
97+ root_group_writable_dirs = _dedupe_existing_paths (
98+ root_group_writable_dirs
99+ ),
88100 )
89101
90102
@@ -133,6 +145,54 @@ def _normalize_path_entries(
133145 return entries
134146
135147
148+ def _normalize_managed_directory_entries (
149+ value : Any ,
150+ * ,
151+ repo_root : str ,
152+ field_name : str ,
153+ ) -> tuple [list [str ], list [str ]]:
154+ if value is None :
155+ return [], []
156+ if not isinstance (value , list ):
157+ raise ValueError (f'{ field_name } must be a list if provided' )
158+
159+ managed_entries : list [str ] = []
160+ root_group_writable_entries : list [str ] = []
161+ for index , item in enumerate (value ):
162+ item_field_name = f'{ field_name } [{ index } ]'
163+ path_value : Any
164+ root_group_writable : bool
165+ if isinstance (item , str ):
166+ path_value = item
167+ root_group_writable = False
168+ elif isinstance (item , dict ):
169+ path_value = item .get ('path' )
170+ raw_root_group_writable = _coerce_optional_bool (
171+ item .get ('root_group_writable' ),
172+ field_name = f'{ item_field_name } .root_group_writable' ,
173+ )
174+ root_group_writable = (
175+ False
176+ if raw_root_group_writable is None
177+ else raw_root_group_writable
178+ )
179+ else :
180+ raise ValueError (
181+ f'{ item_field_name } must be a string or mapping with a path'
182+ )
183+
184+ path = _resolve_path (
185+ value = path_value ,
186+ repo_root = repo_root ,
187+ field_name = f'{ item_field_name } .path' ,
188+ )
189+ managed_entries .append (path )
190+ if root_group_writable :
191+ root_group_writable_entries .append (path )
192+
193+ return managed_entries , root_group_writable_entries
194+
195+
136196def _normalize_load_script_copy_entries (
137197 value : Any ,
138198 * ,
@@ -215,6 +275,26 @@ def _normalize_load_script_symlink_entries(
215275 return list (deduped .values ())
216276
217277
278+ def _coerce_optional_bool (
279+ value : Any ,
280+ * ,
281+ field_name : str ,
282+ ) -> bool | None :
283+ if value is None :
284+ return None
285+ if isinstance (value , bool ):
286+ return value
287+ if isinstance (value , str ):
288+ candidate = value .strip ().lower ()
289+ if candidate in ('true' , 'yes' , 'on' , '1' ):
290+ return True
291+ if candidate in ('false' , 'no' , 'off' , '0' ):
292+ return False
293+ if candidate in ('' , 'none' , 'null' , 'dynamic' ):
294+ return None
295+ raise ValueError (f'{ field_name } must be a boolean if provided' )
296+
297+
218298def _resolve_path (
219299 * ,
220300 value : Any ,
0 commit comments