@@ -12,6 +12,7 @@ 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+ group_writable_dirs : list [str ] = field (default_factory = list )
1516
1617
1718def create_shared_deploy_artifacts (
@@ -29,7 +30,7 @@ def create_shared_deploy_artifacts(
2930 repo_root = repo_root ,
3031 field_name = 'shared.base_path' ,
3132 )
32- managed_dirs = _normalize_path_entries (
33+ managed_dirs , group_writable_dirs = _normalize_managed_directory_entries (
3334 shared_cfg .get ('managed_directories' ),
3435 repo_root = repo_root ,
3536 field_name = 'shared.managed_directories' ,
@@ -81,9 +82,16 @@ def create_shared_deploy_artifacts(
8182 dest_link .symlink_to (target_path )
8283 managed_dirs .append (str (dest_link .parent ))
8384
85+ managed_dirs = _dedupe_existing_paths (managed_dirs )
86+ group_writable_dir_set = set (_dedupe_existing_paths (group_writable_dirs ))
87+ group_writable_dirs = [
88+ path for path in managed_dirs if path in group_writable_dir_set
89+ ]
90+
8491 return SharedDeployArtifacts (
8592 base_path = base_path ,
86- managed_dirs = _dedupe_existing_paths (managed_dirs ),
93+ managed_dirs = managed_dirs ,
94+ group_writable_dirs = group_writable_dirs ,
8795 managed_files = _dedupe_existing_paths (managed_files ),
8896 )
8997
@@ -133,6 +141,52 @@ def _normalize_path_entries(
133141 return entries
134142
135143
144+ def _normalize_managed_directory_entries (
145+ value : Any ,
146+ * ,
147+ repo_root : str ,
148+ field_name : str ,
149+ ) -> tuple [list [str ], list [str ]]:
150+ if value is None :
151+ return [], []
152+ if not isinstance (value , list ):
153+ raise ValueError (f'{ field_name } must be a list if provided' )
154+
155+ entries : list [str ] = []
156+ group_writable_entries : list [str ] = []
157+ for index , item in enumerate (value ):
158+ item_field_name = f'{ field_name } [{ index } ]'
159+ path_value : Any
160+ group_writable : bool
161+ if isinstance (item , str ):
162+ path_value = item
163+ group_writable = False
164+ elif isinstance (item , dict ):
165+ path_value = item .get ('path' )
166+ raw_group_writable = _coerce_optional_bool (
167+ item .get ('group_writable' ),
168+ field_name = f'{ item_field_name } .group_writable' ,
169+ )
170+ group_writable = (
171+ False if raw_group_writable is None else raw_group_writable
172+ )
173+ else :
174+ raise ValueError (
175+ f'{ item_field_name } must be a string or mapping with a path'
176+ )
177+
178+ path = _resolve_path (
179+ value = path_value ,
180+ repo_root = repo_root ,
181+ field_name = f'{ item_field_name } .path' ,
182+ )
183+ entries .append (path )
184+ if group_writable :
185+ group_writable_entries .append (path )
186+
187+ return entries , group_writable_entries
188+
189+
136190def _normalize_load_script_copy_entries (
137191 value : Any ,
138192 * ,
@@ -215,6 +269,26 @@ def _normalize_load_script_symlink_entries(
215269 return list (deduped .values ())
216270
217271
272+ def _coerce_optional_bool (
273+ value : Any ,
274+ * ,
275+ field_name : str ,
276+ ) -> bool | None :
277+ if value is None :
278+ return None
279+ if isinstance (value , bool ):
280+ return value
281+ if isinstance (value , str ):
282+ candidate = value .strip ().lower ()
283+ if candidate in ('true' , 'yes' , 'on' , '1' ):
284+ return True
285+ if candidate in ('false' , 'no' , 'off' , '0' ):
286+ return False
287+ if candidate in ('' , 'none' , 'null' , 'dynamic' ):
288+ return None
289+ raise ValueError (f'{ field_name } must be a boolean if provided' )
290+
291+
218292def _resolve_path (
219293 * ,
220294 value : Any ,
0 commit comments