11"""Resolve ``"auto"`` image tags and chart versions via skopeo/helm."""
22
33import json
4+ import re
45import subprocess
56from copy import deepcopy
67
@@ -20,6 +21,19 @@ class VersionResolver:
2021 def __init__ (self , logger , dry_run : bool = False ):
2122 self .logger = logger
2223
24+ @staticmethod
25+ def _latest_version_tag (tags : list [str ]) -> str | None :
26+ """Select the greatest tag using natural version-aware ordering."""
27+
28+ def version_key (tag : str ) -> tuple :
29+ return tuple (
30+ (1 , int (part ), len (part )) if part .isdigit () else (0 , part .casefold ())
31+ for part in re .split (r"(\d+)" , tag )
32+ if part
33+ )
34+
35+ return max (tags , key = version_key ) if tags else None
36+
2337 def resolve_image_tag (self , registry : str , repository : str ) -> str :
2438 """Resolve the latest tag for an image via skopeo, falling back to podman."""
2539 image_ref = repository
@@ -61,23 +75,24 @@ def _resolve_via_crane(self, image_ref: str) -> str | None:
6175 for line in result .stdout .strip ().split ("\n " )
6276 if line .strip ()
6377 ]
64- if lines :
65- return lines [- 1 ]
78+ return self ._latest_version_tag (lines )
6679 except FileNotFoundError :
6780 pass
6881 return None
6982
7083 def _resolve_via_skopeo (self , image_ref : str ) -> str | None :
7184 """Resolve latest tag using skopeo list-tags."""
72- cmd = f "skopeo list-tags docker://{ image_ref } | jq -r '.Tags[]' | sort -V"
85+ cmd = [ "skopeo" , " list-tags" , f" docker://{ image_ref } " ]
7386 try :
7487 result = subprocess .run (
75- cmd . split () , capture_output = True , text = True , check = True
88+ cmd , capture_output = True , text = True , check = True
7689 )
7790 tags_data = json .loads (result .stdout )
7891 tags = tags_data .get ("Tags" , [])
79- if tags :
80- return tags [- 1 ]
92+ if isinstance (tags , list ):
93+ return self ._latest_version_tag (
94+ [tag for tag in tags if isinstance (tag , str ) and tag ]
95+ )
8196 except (subprocess .CalledProcessError , json .JSONDecodeError , FileNotFoundError ):
8297 pass
8398 return None
@@ -90,12 +105,12 @@ def _resolve_via_podman(self, image_ref: str) -> str | None:
90105 cmd .split (), capture_output = True , text = True , check = False
91106 )
92107 if result .returncode == 0 :
93- lines = result . stdout . strip (). split ( " \n " )
94- if lines :
95- last_line = lines [ - 1 ]
96- parts = last_line . split ()
97- if len (parts ) >= 2 :
98- return parts [ 1 ]
108+ tags = []
109+ for line in result . stdout . strip (). split ( " \n " ) :
110+ parts = line . split ()
111+ if len ( parts ) >= 2 and parts [ 1 ]. casefold () != "tag" :
112+ tags . append (parts [ 1 ])
113+ return self . _latest_version_tag ( tags )
99114 except FileNotFoundError :
100115 pass
101116 return None
0 commit comments