1
+ import json
2
+ import os
3
+ import re
4
+ import requests
5
+ import argparse
6
+ from typing import Dict , List , Optional
7
+
8
+ # Read GitHub token from env
9
+ TFY_GITHUB_TOKEN = os .getenv ("TFY_GITHUB_TOKEN" , "" )
10
+ if not TFY_GITHUB_TOKEN :
11
+ raise EnvironmentError ("❌ TFY_GITHUB_TOKEN not set in environment variables" )
12
+
13
+ GITHUB_API_HEADERS = {
14
+ "Accept" : "application/vnd.github+json" ,
15
+ "Authorization" : f"Bearer { TFY_GITHUB_TOKEN } "
16
+ }
17
+
18
+
19
+ def extract_truefoundry_images (manifest : List [dict ]) -> List [str ]:
20
+ for item in manifest :
21
+ if item .get ("type" ) == "helm" and item .get ("details" , {}).get ("chart" ) == "truefoundry" :
22
+ return item ["details" ].get ("images" , [])
23
+ return []
24
+
25
+
26
+ def map_images_by_name (images : List [str ]) -> Dict [str , str ]:
27
+ image_map = {}
28
+ for image in images :
29
+ if ":" not in image :
30
+ print (f"⚠️ Skipping malformed image: { image } " )
31
+ continue
32
+ path , tag = image .rsplit (":" , 1 )
33
+ name = path .split ("/" )[- 1 ]
34
+ image_map [name ] = tag
35
+ return image_map
36
+
37
+
38
+ def compare_image_tags (old_json_path : str , new_json_path : str ) -> List [Dict [str , str ]]:
39
+ with open (old_json_path ) as f1 , open (new_json_path ) as f2 :
40
+ old_manifest = json .load (f1 )
41
+ new_manifest = json .load (f2 )
42
+
43
+ old_images = extract_truefoundry_images (old_manifest )
44
+ new_images = extract_truefoundry_images (new_manifest )
45
+
46
+ old_map = map_images_by_name (old_images )
47
+ new_map = map_images_by_name (new_images )
48
+
49
+ changes = []
50
+ for name , new_tag in new_map .items ():
51
+ old_tag = old_map .get (name )
52
+ if old_tag and old_tag != new_tag :
53
+ changes .append ({
54
+ "image" : name ,
55
+ "old_tag" : old_tag ,
56
+ "new_tag" : new_tag
57
+ })
58
+ return changes
59
+
60
+
61
+ def get_commits (repo : str , old_tag : str , new_tag : str ) -> List [dict ]:
62
+ url = f"https://api.github.com/repos/truefoundry/{ repo } /compare/{ old_tag } ...{ new_tag } "
63
+ try :
64
+ resp = requests .get (url , headers = GITHUB_API_HEADERS )
65
+ resp .raise_for_status ()
66
+ return resp .json ().get ("commits" , [])
67
+ except requests .RequestException as e :
68
+ print (f"❌ Failed to get commits for { repo } : { e } " )
69
+ return []
70
+
71
+
72
+ def extract_pr_number (message : str ) -> Optional [str ]:
73
+ match = re .search (r"\(#(\d+)\)" , message )
74
+ return match .group (1 ) if match else None
75
+
76
+
77
+ def enrich_commit (commit : dict , repo : str ) -> Dict :
78
+ commit_info = {
79
+ "author" : commit .get ("commit" , {}).get ("author" , {}),
80
+ "message" : commit .get ("commit" , {}).get ("message" , "" ),
81
+ "commit_url" : commit .get ("html_url" , "" ),
82
+ "pull_request" : ""
83
+ }
84
+ pr_num = extract_pr_number (commit_info ["message" ])
85
+ if pr_num :
86
+ commit_info ["pull_request" ] = f"https://github.com/truefoundry/{ repo } /pull/{ pr_num } "
87
+ return commit_info
88
+
89
+
90
+ def generate_changelog (changes : List [Dict [str , str ]]) -> Dict [str , List [Dict ]]:
91
+ changelog = {}
92
+ for entry in changes :
93
+ repo = entry ["image" ]
94
+ old_tag = entry ["old_tag" ]
95
+ new_tag = entry ["new_tag" ]
96
+
97
+ print (f"🔍 Fetching commits for { repo } : { old_tag } → { new_tag } " )
98
+ commits = get_commits (repo , old_tag , new_tag )
99
+
100
+ changelog [repo ] = [enrich_commit (commit , repo ) for commit in commits ]
101
+ return changelog
102
+
103
+
104
+ def write_json (data : dict , filename : str ) -> None :
105
+ with open (filename , "w" ) as f :
106
+ json .dump (data , f , indent = 2 )
107
+ print (f"✅ Saved changelog to { filename } " )
108
+
109
+
110
+ if __name__ == "__main__" :
111
+ parser = argparse .ArgumentParser (description = "Compare image tags and get changelogs from truefoundry manifests." )
112
+ parser .add_argument ("old_manifest" , help = "Path to old artifacts-manifest.json" )
113
+ parser .add_argument ("new_manifest" , help = "Path to new artifacts-manifest.json" )
114
+ parser .add_argument ("--output" , default = "changelog.json" , help = "Output changelog JSON file (default: changelog.json)" )
115
+
116
+ args = parser .parse_args ()
117
+
118
+ image_changes = compare_image_tags (args .old_manifest , args .new_manifest )
119
+ print ("📦 Changed images:" )
120
+ print (json .dumps (image_changes , indent = 2 ))
121
+
122
+ changelog = generate_changelog (image_changes )
123
+ write_json (changelog , args .output )
0 commit comments