1
+ import argparse
2
+ import datetime
3
+ import re
4
+ from collections import defaultdict
5
+ import subprocess
6
+ import shutil
7
+ from dateutil .relativedelta import relativedelta
8
+
9
+ # Check that the `gh` command is in the path
10
+ def check_gh_command ():
11
+ if not shutil .which ('gh' ):
12
+ print ("Error: The `gh` command is not available in the PATH." )
13
+ print ("Please install the GitHub CLI (https://cli.github.com/) and try again." )
14
+ exit (1 )
15
+
16
+ # humanize duration outputs
17
+ def duration_ago (dt ):
18
+ delta = relativedelta (datetime .datetime .now (), dt )
19
+ if delta .years > 0 :
20
+ return f"{ delta .years } year{ 's' if delta .years > 1 else '' } ago"
21
+ elif delta .months > 0 :
22
+ return f"{ delta .months } month{ 's' if delta .months > 1 else '' } ago"
23
+ elif delta .days > 0 :
24
+ return f"{ delta .days } day{ 's' if delta .days > 1 else '' } ago"
25
+ elif delta .hours > 0 :
26
+ return f"{ delta .hours } hour{ 's' if delta .hours > 1 else '' } ago"
27
+ elif delta .minutes > 0 :
28
+ return f"{ delta .minutes } minute{ 's' if delta .minutes > 1 else '' } ago"
29
+ else :
30
+ return "just now"
31
+
32
+ def parse_version (version ):
33
+ # version pattern
34
+ pattern = r"v(\d+)\.(\d+)\.(\d+)"
35
+ match = re .match (pattern , version )
36
+ if match :
37
+ major , minor , patch = map (int , match .groups ())
38
+ return (major , minor , patch )
39
+
40
+ # Calculate the end of life date for a minor release version
41
+ # according to : https://kubernetes-csi.github.io/docs/project-policies.html#support
42
+ def end_of_life_grouped_versions (versions ):
43
+ supported_versions = []
44
+ # Prepare dates for later calculation
45
+ now = datetime .datetime .now ()
46
+ one_year = datetime .timedelta (days = 365 )
47
+ three_months = datetime .timedelta (days = 90 )
48
+
49
+ # get the newer versions on top
50
+ sorted_versions_list = sorted (versions .items (), key = lambda x : x [0 ], reverse = True )
51
+ # structure example :
52
+ # [((3, 5), [('v3.5.0', datetime.datetime(2023, 4, 27, 22, 28, 6))]),
53
+ # ((3, 4),
54
+ # [('v3.4.1', datetime.datetime(2023, 4, 5, 17, 41, 15)),
55
+ # ('v3.4.0', datetime.datetime(2022, 12, 27, 23, 43, 41))])]
56
+ latest = sorted_versions_list .pop (0 )
57
+
58
+ # the latest version is always supported no matter the release date
59
+ supported_versions .append (latest [1 ][- 1 ])
60
+
61
+ for v in sorted_versions_list :
62
+ first_release = v [1 ][- 1 ]
63
+ last_release = v [1 ][0 ]
64
+ # if the release is less than a year old we support the latest patch version
65
+ if now - first_release [1 ] < one_year :
66
+ supported_versions .append (last_release )
67
+ # if the main release is older than a year and has a recent path, this is supported
68
+ elif now - last_release [1 ] < three_months :
69
+ supported_versions .append (last_release )
70
+ return supported_versions
71
+
72
+ def get_release_docker_image (repo , version ):
73
+ output = subprocess .check_output (['gh' , 'release' , '-R' , repo , 'view' , version ], text = True )
74
+ match = re .search (r"`docker pull (.*)`" , output )
75
+ docker_image = match .group (1 )
76
+ return ((version , docker_image ))
77
+
78
+ def get_versions_from_releases (repo ):
79
+ # Run the `gh release` command to get the release list
80
+ output = subprocess .check_output (['gh' , 'release' , '-R' , repo , 'list' ], text = True )
81
+ # Parse the output and group by major and minor version numbers
82
+ versions = defaultdict (lambda : [])
83
+ for line in output .strip ().split ('\n ' ):
84
+ parts = line .split ('\t ' )
85
+ # pprint.pprint(parts)
86
+ version = parts [0 ]
87
+ parsed_version = parse_version (version )
88
+ if parsed_version is None :
89
+ continue
90
+ major , minor , patch = parsed_version
91
+
92
+ published = datetime .datetime .strptime (parts [3 ], '%Y-%m-%dT%H:%M:%SZ' )
93
+ versions [(major , minor )].append ((version , published ))
94
+ return (versions )
95
+
96
+
97
+ def main ():
98
+ # Argument parser
99
+ parser = argparse .ArgumentParser (description = 'Get the currently supported versions for a GitHub repository.' )
100
+ parser .add_argument (
101
+ '--repo' ,
102
+ '-R' , required = True ,
103
+ action = 'append' , dest = 'repos' ,
104
+ help = '''The name of the repository in the format owner/repo. You can specify multiple -R repo to query multiple repositories e.g.:\n
105
+ python -R kubernetes-csi/external-attacher -R kubernetes-csi/external-provisioner -R kubernetes-csi/external-resizer -R kubernetes-csi/external-snapshotter -R kubernetes-csi/livenessprobe -R kubernetes-csi/node-driver-registrar -R kubernetes-csi/external-health-monitor'''
106
+ )
107
+ parser .add_argument ('--display' , '-d' , action = 'store_true' , help = '(default) Display EOL versions with their dates' , default = True )
108
+ parser .add_argument ('--doc' , '-D' , action = 'store_true' , help = 'Helper to https://kubernetes-csi.github.io/docs/ that prints Docker image for each EOL version' )
109
+
110
+ args = parser .parse_args ()
111
+
112
+ # Verify pre-reqs
113
+ check_gh_command ()
114
+
115
+ # Process all repos
116
+ for repo in args .repos :
117
+ versions = get_versions_from_releases (repo )
118
+ eol_versions = end_of_life_grouped_versions (versions )
119
+
120
+ if args .display :
121
+ print (f"Supported versions with release date and age of `{ repo } `:\n " )
122
+ for version in eol_versions :
123
+ print (f"{ version [0 ]} \t { version [1 ].strftime ('%Y-%m-%d' )} \t { duration_ago (version [1 ])} " )
124
+
125
+ # TODO : generate proper doc ouput for the tables of: https://kubernetes-csi.github.io/docs/sidecar-containers.html
126
+ if args .doc :
127
+ print ("\n Supported Versions with docker images for each end of life version:\n " )
128
+ for version in eol_versions :
129
+ _ , image = get_release_docker_image (args .repo , version [0 ])
130
+ print (f"{ version [0 ]} \t { image } " )
131
+ print ()
132
+
133
+ if __name__ == '__main__' :
134
+ main ()
0 commit comments