88import fnmatch
99import gzip
1010import json
11- import re
1211from collections import OrderedDict , defaultdict
1312from collections .abc import Iterator
1413from pathlib import Path
@@ -182,6 +181,69 @@ def build_integrations_schemas(overwrite: bool, integration: str | None = None)
182181 print (f"final integrations manifests dumped: { SCHEMA_FILE_PATH } " )
183182
184183
184+ def _parse_clause (clause : str ) -> tuple [Version , Version | None ]:
185+ """Parse a single AND'd clause of npm-style range tokens into ``[lo, hi)`` bounds.
186+
187+ ``hi`` is ``None`` when the clause has no upper bound. Supports the subset of
188+ npm semver currently emitted by EPR ``conditions.kibana.version`` strings:
189+ ``^X.Y.Z``, ``~X.Y.Z``, ``>=X.Y.Z``, ``>X.Y.Z``, ``<=X.Y.Z``, ``<X.Y.Z``,
190+ ``=X.Y.Z``, and bare ``X.Y.Z``. Unsupported tokens raise ``ValueError`` so
191+ we fail loudly if EPR's grammar grows.
192+ """
193+ lo = Version (0 , 0 , 0 )
194+ hi : Version | None = None
195+
196+ def tighten_hi (current : Version | None , candidate : Version ) -> Version :
197+ return candidate if current is None else min (current , candidate )
198+
199+ for token in clause .strip ().split ():
200+ if not token :
201+ continue
202+ if token .startswith ("^" ):
203+ base = Version .parse (token [1 :])
204+ if base .major == 0 :
205+ raise ValueError (f"caret on 0.x kibana version is unsupported: { token !r} " )
206+ lo = max (lo , base )
207+ hi = tighten_hi (hi , Version (base .major + 1 , 0 , 0 ))
208+ elif token .startswith ("~" ):
209+ base = Version .parse (token [1 :])
210+ lo = max (lo , base )
211+ hi = tighten_hi (hi , Version (base .major , base .minor + 1 , 0 ))
212+ elif token .startswith (">=" ):
213+ lo = max (lo , Version .parse (token [2 :]))
214+ elif token .startswith ("<=" ):
215+ hi = tighten_hi (hi , Version .parse (token [2 :]).bump_patch ())
216+ elif token .startswith (">" ):
217+ lo = max (lo , Version .parse (token [1 :]).bump_patch ())
218+ elif token .startswith ("<" ):
219+ hi = tighten_hi (hi , Version .parse (token [1 :]))
220+ elif token .startswith ("=" ):
221+ exact = Version .parse (token [1 :])
222+ lo = max (lo , exact )
223+ hi = tighten_hi (hi , exact .bump_patch ())
224+ elif token [0 ].isdigit ():
225+ exact = Version .parse (token )
226+ lo = max (lo , exact )
227+ hi = tighten_hi (hi , exact .bump_patch ())
228+ else :
229+ raise ValueError (f"unsupported kibana version token: { token !r} " )
230+ return lo , hi
231+
232+
233+ def _parse_kibana_range (version_requirement : str ) -> list [tuple [Version , Version | None ]]:
234+ """Parse an EPR ``conditions.kibana.version`` string into a list of ``[lo, hi)`` clauses.
235+
236+ Clauses separated by ``||`` are OR'd; whitespace-separated tokens within a
237+ clause are AND'd.
238+ """
239+ return [_parse_clause (c ) for c in version_requirement .split ("||" )]
240+
241+
242+ def _satisfies_kibana_range (stack : Version , version_requirement : str ) -> bool :
243+ """Return True iff ``stack`` satisfies the EPR ``conditions.kibana.version`` string."""
244+ return any (lo <= stack and (hi is None or stack < hi ) for lo , hi in _parse_kibana_range (version_requirement ))
245+
246+
185247def find_least_compatible_version (
186248 package : str ,
187249 integration : str ,
@@ -207,14 +269,9 @@ def find_least_compatible_version(
207269 for version , manifest in OrderedDict (
208270 sorted (major_integration_manifests .items (), key = lambda x : Version .parse (x [0 ]))
209271 ).items ():
210- compatible_versions = re .sub (r"\>|\<|\=|\^|\~" , "" , manifest ["conditions" ]["kibana" ]["version" ]).split (
211- " || "
212- )
213- for kibana_ver in compatible_versions :
214- _kibana_ver = Version .parse (kibana_ver )
215- # check versions have the same major
216- if _kibana_ver .major == stack_version .major and _kibana_ver <= stack_version :
217- return f"^{ version } "
272+ version_requirement = manifest ["conditions" ]["kibana" ]["version" ]
273+ if _satisfies_kibana_range (stack_version , version_requirement ):
274+ return f"^{ version } "
218275
219276 raise ValueError (f"no compatible version for integration { package } :{ integration } " )
220277
@@ -236,38 +293,32 @@ def find_latest_compatible_version(
236293
237294 # Converts the dict keys (version numbers) to Version objects for proper sorting (descending)
238295 integration_manifests = sorted (package_manifest .items (), key = lambda x : Version .parse (x [0 ]), reverse = True )
239- notice = ["" ]
296+ notice : list [str ] = ["" ]
297+ newest_skipped : tuple [str , Version ] | None = None
240298
241299 for version , manifest in integration_manifests :
242300 kibana_conditions = manifest .get ("conditions" , {}).get ("kibana" , {})
243301 version_requirement = kibana_conditions .get ("version" )
244302 if not version_requirement :
245303 raise ValueError (f"Manifest for { package } :{ integration } version { version } is missing conditions." )
246304
247- compatible_versions = re .sub (r"\>|\<|\=|\^|\~" , "" , version_requirement ).split (" || " )
248-
249- if not compatible_versions :
250- raise ValueError (f"Manifest for { package } :{ integration } version { version } is missing compatible versions" )
251-
252- highest_compatible_version = Version .parse (max (compatible_versions , key = Version .parse ))
253-
254- if highest_compatible_version > rule_stack_version :
255- # generate notice message that a later integration version is available
256- integration = f" { integration .strip ()} " if integration else ""
257-
258- notice = [
259- f"There is a new integration { package } { integration } version { version } available!" ,
260- f"Update the rule min_stack version from { rule_stack_version } to "
261- f"{ highest_compatible_version } if using new features in this latest version." ,
262- ]
263-
264- if highest_compatible_version .major == rule_stack_version .major :
305+ if _satisfies_kibana_range (rule_stack_version , version_requirement ):
306+ if newest_skipped is not None :
307+ skipped_version , skipped_floor = newest_skipped
308+ integration_label = f" { integration .strip ()} " if integration else ""
309+ notice = [
310+ f"There is a new integration { package } { integration_label } version { skipped_version } available!" ,
311+ f"Update the rule min_stack version from { rule_stack_version } to "
312+ f"{ skipped_floor } if using new features in this latest version." ,
313+ ]
265314 return version , notice
266315
267- # Check for rules that cross majors
268- for compatible_version in compatible_versions :
269- if Version .parse (compatible_version ) <= rule_stack_version :
270- return version , notice
316+ # Track the newest manifest we had to skip so the notice can still
317+ # point the reader at the most recent incompatible version and its floor.
318+ if newest_skipped is None :
319+ clauses = _parse_kibana_range (version_requirement )
320+ floor = min (lo for lo , _ in clauses )
321+ newest_skipped = (version , floor )
271322
272323 raise ValueError (f"no compatible version for integration { package } :{ integration } " )
273324
0 commit comments