33import base64
44import copy
55import io
6+ from collections .abc import Mapping
67
78import github3
89import ruamel .yaml
1920stream = io .StringIO ()
2021
2122
23+ COOLDOWN_DAYS_KEYS_ORDERED = (
24+ "default-days" ,
25+ "semver-major-days" ,
26+ "semver-minor-days" ,
27+ "semver-patch-days" ,
28+ )
29+ VALID_COOLDOWN_DAYS_KEYS = frozenset (COOLDOWN_DAYS_KEYS_ORDERED )
30+ VALID_COOLDOWN_KEYS = VALID_COOLDOWN_DAYS_KEYS | {"include" , "exclude" }
31+ MAX_COOLDOWN_LIST_ITEMS = 150
32+ MIN_COOLDOWN_DAYS = 1
33+ MAX_COOLDOWN_DAYS = 90
34+
35+
36+ def validate_cooldown_config (cooldown ):
37+ """
38+ Validate the cooldown configuration from the dependabot config file.
39+
40+ Args:
41+ cooldown: dict with cooldown configuration
42+
43+ Raises:
44+ ValueError: if the cooldown configuration is invalid
45+ """
46+ if not isinstance (cooldown , Mapping ):
47+ raise ValueError ("Cooldown configuration must be a mapping" )
48+
49+ unknown_keys = set (cooldown .keys ()) - VALID_COOLDOWN_KEYS
50+ if unknown_keys :
51+ raise ValueError (
52+ f"Unknown cooldown configuration keys: { ', ' .join (sorted (unknown_keys ))} "
53+ )
54+
55+ has_days = False
56+ for key in VALID_COOLDOWN_DAYS_KEYS :
57+ if key in cooldown :
58+ value = cooldown [key ]
59+ if not isinstance (value , int ) or isinstance (value , bool ):
60+ raise ValueError (
61+ f"Cooldown '{ key } ' must be an integer between "
62+ f"{ MIN_COOLDOWN_DAYS } and { MAX_COOLDOWN_DAYS } , "
63+ f"got { type (value ).__name__ } "
64+ )
65+ if value < MIN_COOLDOWN_DAYS or value > MAX_COOLDOWN_DAYS :
66+ raise ValueError (
67+ f"Cooldown '{ key } ' must be between "
68+ f"{ MIN_COOLDOWN_DAYS } and { MAX_COOLDOWN_DAYS } "
69+ )
70+ has_days = True
71+
72+ if not has_days :
73+ raise ValueError (
74+ "Cooldown configuration must include at least one of: "
75+ + ", " .join (sorted (VALID_COOLDOWN_DAYS_KEYS ))
76+ )
77+
78+ for list_key in ("include" , "exclude" ):
79+ if list_key in cooldown :
80+ items = cooldown [list_key ]
81+ if not isinstance (items , list ):
82+ raise ValueError (f"Cooldown '{ list_key } ' must be a list" )
83+ if len (items ) > MAX_COOLDOWN_LIST_ITEMS :
84+ raise ValueError (
85+ f"Cooldown '{ list_key } ' must have at most { MAX_COOLDOWN_LIST_ITEMS } items"
86+ )
87+ for item in items :
88+ if not isinstance (item , str ):
89+ raise ValueError (f"Cooldown '{ list_key } ' items must be strings" )
90+
91+
2292def make_dependabot_config (
2393 ecosystem ,
2494 group_dependencies ,
@@ -27,9 +97,12 @@ def make_dependabot_config(
2797 labels ,
2898 dependabot_config ,
2999 extra_dependabot_config ,
30- ) -> str :
100+ cooldown = None ,
101+ ) -> None :
31102 """
32- Make the dependabot configuration for a specific package ecosystem
103+ Make the dependabot configuration for a specific package ecosystem.
104+
105+ Mutates dependabot_config in place by appending an update entry.
33106
34107 Args:
35108 ecosystem: the package ecosystem to make the dependabot configuration for
@@ -39,9 +112,7 @@ def make_dependabot_config(
39112 labels: the list of labels to be added to dependabot configuration
40113 dependabot_config: extra dependabot configs
41114 extra_dependabot_config: File with the configuration to add dependabot configs (ex: private registries)
42-
43- Returns:
44- str: the dependabot configuration for the package ecosystem
115+ cooldown: optional cooldown configuration dict to delay version update PRs
45116 """
46117
47118 dependabot_config ["updates" ].append (
@@ -97,7 +168,17 @@ def make_dependabot_config(
97168 }
98169 )
99170
100- return yaml .dump (dependabot_config , stream )
171+ if cooldown :
172+ cooldown_config = {}
173+ for key in COOLDOWN_DAYS_KEYS_ORDERED :
174+ if key in cooldown :
175+ cooldown_config [key ] = cooldown [key ]
176+ for list_key in ("include" , "exclude" ):
177+ if list_key in cooldown :
178+ cooldown_config [list_key ] = [
179+ SingleQuotedScalarString (item ) for item in cooldown [list_key ]
180+ ]
181+ dependabot_config ["updates" ][- 1 ].update ({"cooldown" : cooldown_config })
101182
102183
103184def build_dependabot_file (
@@ -110,6 +191,7 @@ def build_dependabot_file(
110191 schedule_day ,
111192 labels ,
112193 extra_dependabot_config ,
194+ cooldown = None ,
113195) -> str | None :
114196 """
115197 Build the dependabot.yml file for a repo based on the repo contents
@@ -124,6 +206,7 @@ def build_dependabot_file(
124206 schedule_day: the day of the week to run dependabot ex: "monday" if schedule is "daily"
125207 labels: the list of labels to be added to dependabot configuration
126208 extra_dependabot_config: File with the configuration to add dependabot configs (ex: private registries)
209+ cooldown: optional cooldown configuration dict to delay version update PRs
127210
128211 Returns:
129212 str: the dependabot.yml file for the repo
@@ -203,6 +286,7 @@ def build_dependabot_file(
203286 labels ,
204287 dependabot_file ,
205288 extra_dependabot_config ,
289+ cooldown ,
206290 )
207291 break
208292 except OptionalFileNotFoundError :
@@ -224,6 +308,7 @@ def build_dependabot_file(
224308 labels ,
225309 dependabot_file ,
226310 extra_dependabot_config ,
311+ cooldown ,
227312 )
228313 break
229314 except github3 .exceptions .NotFoundError :
@@ -243,6 +328,7 @@ def build_dependabot_file(
243328 labels ,
244329 dependabot_file ,
245330 extra_dependabot_config ,
331+ cooldown ,
246332 )
247333 break
248334 except github3 .exceptions .NotFoundError :
@@ -262,6 +348,7 @@ def build_dependabot_file(
262348 labels ,
263349 dependabot_file ,
264350 extra_dependabot_config ,
351+ cooldown ,
265352 )
266353 break
267354 except github3 .exceptions .NotFoundError :
0 commit comments