3636from doozerlib .image import ImageMetadata
3737from doozerlib .record_logger import RecordLogger
3838from elliottlib .shipment_utils import get_shipment_config_from_mr
39+ from semver import VersionInfo
3940from tenacity import retry , stop_after_attempt , wait_fixed
4041
4142
@@ -655,6 +656,7 @@ def __init__(
655656 fbc_repo : str ,
656657 upcycle : bool ,
657658 ocp_version_override : Optional [Tuple [int , int ]] = None ,
659+ insert_missing_entry : bool = False ,
658660 record_logger : Optional [RecordLogger ] = None ,
659661 logger : Optional [logging .Logger ] = None ,
660662 ) -> None :
@@ -670,11 +672,60 @@ def __init__(
670672 self .ocp_version_override = ocp_version_override
671673 self ._record_logger = record_logger
672674 self ._logger = logger or LOGGER .getChild (self .__class__ .__name__ )
675+ self .insert_missing_entry = insert_missing_entry
673676
674677 @staticmethod
675678 def get_fbc_name (image_name : str ):
676679 return f"{ image_name } -fbc"
677680
681+ @staticmethod
682+ def _extract_version_from_bundle_name (bundle_name : str ) -> VersionInfo :
683+ """Extract semantic version from bundle name.
684+
685+ Bundle names follow format: {operator-name}.v{major}.{minor}.{patch}
686+ Example: "oadp-operator.v1.3.9" -> VersionInfo(1, 3, 9)
687+
688+ :param bundle_name: The bundle name
689+ :return: VersionInfo object
690+ :raises ValueError: If version cannot be parsed from bundle name
691+ """
692+ # Find the version part (starts with .v followed by semantic version)
693+ # Split by '.' and look for the part starting with 'v'
694+ parts = bundle_name .split ('.' )
695+ for i , part in enumerate (parts ):
696+ if part .startswith ('v' ) and len (parts ) > i + 2 :
697+ # Extract vX.Y.Z format
698+ version_str = '.' .join ([part [1 :]] + parts [i + 1 : i + 3 ]) # Remove 'v' prefix
699+ try :
700+ return VersionInfo .parse (version_str )
701+ except Exception as e :
702+ raise ValueError (
703+ f"Cannot parse semantic version from bundle name '{ bundle_name } '. "
704+ f"Expected format: {{operator-name}}.v{{major}}.{{minor}}.{{patch}}"
705+ ) from e
706+ # If we reach here, no version was found
707+ raise ValueError (
708+ f"Cannot parse semantic version from bundle name '{ bundle_name } '. "
709+ f"Expected format: {{operator-name}}.v{{major}}.{{minor}}.{{patch}}"
710+ )
711+
712+ @staticmethod
713+ def _rebuild_replaces_chain (entries : list ) -> None :
714+ """Rebuild replaces field for all entries based on their sorted order.
715+
716+ After sorting entries by version, each entry should replace the previous
717+ entry in the list, forming a linear upgrade chain.
718+
719+ :param entries: List of channel entries sorted by version
720+ """
721+ for i in range (len (entries )):
722+ if i == 0 :
723+ # First entry (oldest version) replaces nothing
724+ entries [i ].pop ('replaces' , None )
725+ else :
726+ # Each subsequent entry replaces the previous one
727+ entries [i ]['replaces' ] = entries [i - 1 ]['name' ]
728+
678729 def _find_future_release_assembly (
679730 self ,
680731 releases_config ,
@@ -1140,24 +1191,41 @@ def _update_channel(channel: Dict):
11401191 assembly_entry ["skipRange" ] = assembly_csv_info .skip_range
11411192 channel ['entries' ].append (assembly_entry )
11421193
1143- # For an operator bundle that uses replaces -- such as OADP
1144- # Update "replaces" in the channel
1145- replaces = None
1146- if not self .group .startswith ('openshift-' ):
1147- # Find the current head - the entry that is not replaced by any other entry
1148- bundle_with_replaces = [it for it in channel ['entries' ]]
1149- replaced_names = {it .get ('replaces' ) for it in bundle_with_replaces if it .get ('replaces' )}
1150- current_head = next ((it for it in bundle_with_replaces if it ['name' ] not in replaced_names ), None )
1151- if current_head :
1152- # The new bundle should replace the current head
1153- replaces = current_head ['name' ]
1154-
11551194 # Add the current bundle to the specified channel in the catalog
11561195 entry = next ((entry for entry in channel ['entries' ] if entry ['name' ] == olm_bundle_name ), None )
11571196 if not entry :
11581197 logger .info ("Adding bundle %s to channel %s" , olm_bundle_name , channel ['name' ])
11591198 entry = {"name" : olm_bundle_name }
1160- channel ['entries' ].append (entry )
1199+
1200+ if self .insert_missing_entry :
1201+ # Add entry and then sort all entries in ascending version order
1202+ channel ['entries' ].append (entry )
1203+
1204+ logger .info ("Sorting channel entries by version order" )
1205+ channel ['entries' ].sort (key = lambda e : self ._extract_version_from_bundle_name (e ['name' ]))
1206+
1207+ # Rebuild replaces chain for non-OpenShift groups (operators using replaces)
1208+ if not self .group .startswith ('openshift-' ):
1209+ logger .info ("Rebuilding replaces chain after version sorting" )
1210+ self ._rebuild_replaces_chain (channel ['entries' ])
1211+ else :
1212+ # For an operator bundle that uses replaces -- such as OADP
1213+ # Calculate replaces based on current head before appending
1214+ replaces = None
1215+ if not self .group .startswith ('openshift-' ):
1216+ # Find the current head - the entry that is not replaced by any other entry
1217+ bundle_with_replaces = [it for it in channel ['entries' ]]
1218+ replaced_names = {it .get ('replaces' ) for it in bundle_with_replaces if it .get ('replaces' )}
1219+ current_head = next (
1220+ (it for it in bundle_with_replaces if it ['name' ] not in replaced_names ), None
1221+ )
1222+ if current_head :
1223+ # The new bundle should replace the current head
1224+ replaces = current_head ['name' ]
1225+
1226+ channel ['entries' ].append (entry )
1227+ if replaces :
1228+ entry ["replaces" ] = replaces
11611229 else :
11621230 logger .warning ("Bundle %s already exists in channel %s. Replacing..." , olm_bundle_name , channel ['name' ])
11631231 entry .clear ()
@@ -1166,8 +1234,6 @@ def _update_channel(channel: Dict):
11661234 entry ["skipRange" ] = olm_skip_range
11671235 if skips :
11681236 entry ["skips" ] = sorted (skips )
1169- if replaces :
1170- entry ["replaces" ] = replaces
11711237
11721238 for channel_name in channel_names :
11731239 logger .info ("Updating channel %s" , channel_name )
0 commit comments