66import ast
77import pathlib
88import re
9+ import subprocess
910from collections .abc import Sequence
1011
1112try :
@@ -108,6 +109,98 @@ def verify_dist(args: argparse.Namespace) -> None:
108109 print (f" { name } " )
109110
110111
112+ def _git (args : Sequence [str ], * , cwd : pathlib .Path | None = None ) -> str :
113+ return subprocess .check_output (
114+ ["git" , * args ],
115+ cwd = cwd ,
116+ encoding = "utf-8" ,
117+ stderr = subprocess .STDOUT ,
118+ ).strip ()
119+
120+
121+ def _version_tuple (version : str ) -> tuple [int , ...] | None :
122+ match = re .fullmatch (r"([0-9]+(?:\.[0-9]+)+)(?:[a-zA-Z0-9_.+-]+)?" , version )
123+ if not match :
124+ return None
125+ return tuple (int (part ) for part in match .group (1 ).split ("." ))
126+
127+
128+ def _previous_release_tag (version : str ) -> str :
129+ current = _version_tuple (version )
130+ if current is None :
131+ raise RuntimeError (f"Cannot determine previous release for { version !r} " )
132+
133+ candidates : list [tuple [int , ...]] = []
134+ for tag in _git (["tag" ]).splitlines ():
135+ tag_version = _version_tuple (tag )
136+ if tag_version is not None and tag_version < current :
137+ candidates .append (tag_version )
138+ if not candidates :
139+ raise RuntimeError (f"Could not find a previous release tag before { version !r} " )
140+ return "." .join (str (part ) for part in max (candidates ))
141+
142+
143+ def _gitlink (rev : str , path : str ) -> str :
144+ output = _git (["ls-tree" , rev , path ])
145+ parts = output .split ()
146+ if len (parts ) < 3 or parts [0 ] != "160000" :
147+ raise RuntimeError (f"Could not find submodule gitlink { path !r} at { rev !r} " )
148+ return parts [2 ]
149+
150+
151+ def _clean_commit_subject (subject : str ) -> str :
152+ subject = subject .encode ("ascii" , "ignore" ).decode ("ascii" )
153+ subject = re .sub (r"\s+" , " " , subject ).strip ()
154+ subject = re .sub (r"^:[a-z0-9_+-]+:\s*" , "" , subject )
155+ return subject .replace (" : " , ": " )
156+
157+
158+ def _link_sdk_core_prs (subject : str ) -> str :
159+ return re .sub (
160+ r"\(#([0-9]+)\)" ,
161+ r"([#\1](https://github.com/temporalio/sdk-rust/pull/\1))" ,
162+ subject ,
163+ )
164+
165+
166+ def _sdk_core_release_notes (version : str , path : str ) -> list [str ]:
167+ previous_tag = _previous_release_tag (version )
168+ previous_commit = _gitlink (previous_tag , path )
169+ current_commit = _gitlink ("HEAD" , path )
170+ if previous_commit == current_commit :
171+ return []
172+
173+ submodule_path = pathlib .Path (path )
174+ if not (submodule_path / ".git" ).exists ():
175+ raise RuntimeError (
176+ f"Submodule { path !r} is not initialized; checkout with submodules"
177+ )
178+
179+ log_args = [
180+ "log" ,
181+ "--format=%H%x00%h%x00%s" ,
182+ "--reverse" ,
183+ f"{ previous_commit } ..{ current_commit } " ,
184+ ]
185+ try :
186+ log_output = _git (log_args , cwd = submodule_path )
187+ except subprocess .CalledProcessError :
188+ _git (["fetch" , "--quiet" , "origin" , "main" ], cwd = submodule_path )
189+ log_output = _git (log_args , cwd = submodule_path )
190+ if not log_output :
191+ return []
192+
193+ lines = ["### SDK Core" , "" ]
194+ for line in log_output .splitlines ():
195+ full_hash , short_hash , subject = line .split ("\0 " , 2 )
196+ subject = _link_sdk_core_prs (_clean_commit_subject (subject ))
197+ lines .append (
198+ f"- [`{ short_hash } `](https://github.com/temporalio/sdk-rust/commit/"
199+ f"{ full_hash } ) { subject } "
200+ )
201+ return lines
202+
203+
111204def changelog_notes (args : argparse .Namespace ) -> None :
112205 changelog_path = pathlib .Path (args .changelog )
113206 lines = changelog_path .read_text (encoding = "utf-8" ).splitlines ()
@@ -140,7 +233,12 @@ def changelog_notes(args: argparse.Namespace) -> None:
140233 if not section_lines :
141234 raise RuntimeError (f"Changelog section for { args .version !r} is empty" )
142235
143- notes = "## Changelog\n \n " + "\n " .join (section_lines ) + "\n "
236+ note_lines = ["## Notable Changes" , "" , * section_lines ]
237+ sdk_core_notes = _sdk_core_release_notes (args .version , args .sdk_core_path )
238+ if sdk_core_notes :
239+ note_lines .extend (["" , * sdk_core_notes ])
240+
241+ notes = "\n " .join (note_lines ) + "\n "
144242 pathlib .Path (args .output ).write_text (notes , encoding = "utf-8" )
145243
146244
@@ -162,6 +260,9 @@ def main(argv: Sequence[str] | None = None) -> None:
162260 changelog_parser .add_argument ("--version" , required = True )
163261 changelog_parser .add_argument ("--changelog" , default = "CHANGELOG.md" )
164262 changelog_parser .add_argument ("--output" , required = True )
263+ changelog_parser .add_argument (
264+ "--sdk-core-path" , default = "temporalio/bridge/sdk-core"
265+ )
165266 changelog_parser .set_defaults (func = changelog_notes )
166267
167268 args = parser .parse_args (argv )
0 commit comments