@@ -13,9 +13,10 @@ use smithy_rs_tool_common::changelog::{
1313    Changelog ,  HandAuthoredEntry ,  Reference ,  SdkModelChangeKind ,  SdkModelEntry , 
1414} ; 
1515use  smithy_rs_tool_common:: git:: { find_git_repository_root,  Git ,  GitCLI } ; 
16+ use  smithy_rs_tool_common:: versions_manifest:: { CrateVersionMetadataMap ,  VersionsManifest } ; 
1617use  std:: env; 
1718use  std:: fmt:: Write ; 
18- use  std:: path:: PathBuf ; 
19+ use  std:: path:: { Path ,   PathBuf } ; 
1920use  time:: OffsetDateTime ; 
2021
2122pub  const  EXAMPLE_ENTRY :  & str  = r#" 
@@ -67,6 +68,10 @@ pub struct RenderArgs {
6768    /// Optional path to output a release manifest file to 
6869#[ clap( long,  action) ]  
6970    pub  release_manifest_output :  Option < PathBuf > , 
71+     /// Optional path to the SDK's versions.toml file for the current release. 
72+ /// This is used to generate a markdown table showing crate versions. 
73+ #[ clap( long,  action) ]  
74+     pub  current_release_versions_manifest :  Option < PathBuf > , 
7075    /// Optional path to the SDK's versions.toml file for the previous release. 
7176/// This is used to filter out changelog entries that have `since_commit` information. 
7277#[ clap( long,  action) ]  
@@ -217,6 +222,16 @@ fn indented_message(message: &str) -> String {
217222    out
218223} 
219224
225+ fn  render_table_row ( columns :  [ & str ;  2 ] ,  out :  & mut  String )  { 
226+     let  mut  row = "|" . to_owned ( ) ; 
227+     for  column in  columns { 
228+         row. push_str ( column) ; 
229+         row. push ( '|' ) ; 
230+     } 
231+     write ! ( out,  "{row}" ) . unwrap ( ) ; 
232+     out. push ( '\n' ) ; 
233+ } 
234+ 
220235fn  load_changelogs ( args :  & RenderArgs )  -> Result < Changelog >  { 
221236    let  mut  combined = Changelog :: new ( ) ; 
222237    for  source in  & args. source  { 
@@ -233,6 +248,19 @@ fn load_changelogs(args: &RenderArgs) -> Result<Changelog> {
233248    Ok ( combined) 
234249} 
235250
251+ fn  load_current_crate_version_metadata_map ( 
252+     current_release_versions_manifest :  Option < & Path > , 
253+ )  -> CrateVersionMetadataMap  { 
254+     current_release_versions_manifest
255+         . and_then ( 
256+             |manifest_path| match  VersionsManifest :: from_file ( manifest_path)  { 
257+                 Ok ( manifest)  => Some ( manifest. crates ) , 
258+                 Err ( _)  => None , 
259+             } , 
260+         ) 
261+         . unwrap_or_default ( ) 
262+ } 
263+ 
236264fn  update_changelogs ( 
237265    args :  & RenderArgs , 
238266    smithy_rs :  & dyn  Git , 
@@ -250,7 +278,13 @@ fn update_changelogs(
250278        args. change_set , 
251279        args. previous_release_versions_manifest . as_deref ( ) , 
252280    ) ?; 
253-     let  ( release_header,  release_notes)  = render ( & entries,  & release_metadata. title ) ; 
281+     let  current_crate_version_metadata_map =
282+         load_current_crate_version_metadata_map ( args. current_release_versions_manifest . as_deref ( ) ) ; 
283+     let  ( release_header,  release_notes)  = render ( 
284+         & entries, 
285+         current_crate_version_metadata_map, 
286+         & release_metadata. title , 
287+     ) ; 
254288    if  let  Some ( output_path)  = & args. release_manifest_output  { 
255289        let  release_manifest = ReleaseManifest  { 
256290            tag_name :  release_metadata. tag . clone ( ) , 
@@ -329,9 +363,94 @@ fn render_sdk_model_entries<'a>(
329363    } 
330364} 
331365
332- /// Convert a list of changelog entries into markdown. 
366+ fn  render_external_contributors ( entries :  & [ ChangelogEntry ] ,  out :  & mut  String )  { 
367+     let  mut  external_contribs = entries
368+         . iter ( ) 
369+         . filter_map ( |entry| entry. hand_authored ( ) . map ( |e| & e. author ) ) 
370+         . filter ( |author| !is_maintainer ( author) ) 
371+         . collect :: < Vec < _ > > ( ) ; 
372+     if  external_contribs. is_empty ( )  { 
373+         return ; 
374+     } 
375+     external_contribs. sort ( ) ; 
376+     external_contribs. dedup ( ) ; 
377+     out. push_str ( "**Contributors**\n Thank you for your contributions! ❤\n " ) ; 
378+     for  contributor_handle in  external_contribs { 
379+         // retrieve all contributions this author made 
380+         let  mut  contribution_references = entries
381+             . iter ( ) 
382+             . filter ( |entry| { 
383+                 entry
384+                     . hand_authored ( ) 
385+                     . map ( |e| e. author . eq_ignore_ascii_case ( contributor_handle. as_str ( ) ) ) 
386+                     . unwrap_or ( false ) 
387+             } ) 
388+             . flat_map ( |entry| { 
389+                 entry
390+                     . hand_authored ( ) 
391+                     . unwrap ( ) 
392+                     . references 
393+                     . iter ( ) 
394+                     . map ( to_md_link) 
395+             } ) 
396+             . collect :: < Vec < _ > > ( ) ; 
397+         contribution_references. sort ( ) ; 
398+         contribution_references. dedup ( ) ; 
399+         let  contribution_references = contribution_references. as_slice ( ) . join ( ", " ) ; 
400+         out. push_str ( "- @" ) ; 
401+         out. push_str ( contributor_handle) ; 
402+         if  !contribution_references. is_empty ( )  { 
403+             write ! ( out,  " ({})" ,  contribution_references) 
404+                 // The `Write` implementation for `String` is infallible, 
405+                 // see https://doc.rust-lang.org/src/alloc/string.rs.html#2815 
406+                 . unwrap ( ) 
407+         } 
408+         out. push ( '\n' ) ; 
409+     } 
410+     out. push ( '\n' ) ; 
411+ } 
412+ 
413+ fn  render_details ( summary :  & str ,  body :  & str ,  out :  & mut  String )  { 
414+     out. push_str ( "<details>" ) ; 
415+     out. push ( '\n' ) ; 
416+     write ! ( out,  "<summary>{}</summary>" ,  summary) . unwrap ( ) ; 
417+     out. push ( '\n' ) ; 
418+     // A blank line is required for the body to be rendered properly 
419+     out. push ( '\n' ) ; 
420+     out. push_str ( body) ; 
421+     out. push_str ( "</details>" ) ; 
422+     out. push ( '\n' ) ; 
423+ } 
424+ 
425+ fn  render_crate_versions ( crate_version_metadata_map :  CrateVersionMetadataMap ,  out :  & mut  String )  { 
426+     if  crate_version_metadata_map. is_empty ( )  { 
427+         // If the map is empty, we choose to not render anything, as opposed to 
428+         // rendering the <details> element with empty contents and a user toggling 
429+         // it only to find out there is nothing in it. 
430+         return ; 
431+     } 
432+ 
433+     out. push_str ( "**Crate Versions**" ) ; 
434+     out. push ( '\n' ) ; 
435+ 
436+     let  mut  table = String :: new ( ) ; 
437+     render_table_row ( [ "Crate" ,  "Version" ] ,  & mut  table) ; 
438+     render_table_row ( [ "-" ,  "-" ] ,  & mut  table) ; 
439+     for  ( crate_name,  version_metadata)  in  & crate_version_metadata_map { 
440+         render_table_row ( [ crate_name,  & version_metadata. version ] ,  & mut  table) ; 
441+     } 
442+ 
443+     render_details ( "Click to expand to view crate versions..." ,  & table,  out) ; 
444+     out. push ( '\n' ) ; 
445+ } 
446+ 
447+ /// Convert a list of changelog entries and crate versions into markdown. 
333448/// Returns (header, body) 
334- fn  render ( entries :  & [ ChangelogEntry ] ,  release_header :  & str )  -> ( String ,  String )  { 
449+ fn  render ( 
450+     entries :  & [ ChangelogEntry ] , 
451+     crate_version_metadata_map :  CrateVersionMetadataMap , 
452+     release_header :  & str , 
453+ )  -> ( String ,  String )  { 
335454    let  mut  header = String :: new ( ) ; 
336455    header. push_str ( release_header) ; 
337456    header. push ( '\n' ) ; 
@@ -349,61 +468,24 @@ fn render(entries: &[ChangelogEntry], release_header: &str) -> (String, String)
349468        entries. iter ( ) . filter_map ( ChangelogEntry :: aws_sdk_model) , 
350469        & mut  out, 
351470    ) ; 
352- 
353-     let  mut  external_contribs = entries
354-         . iter ( ) 
355-         . filter_map ( |entry| entry. hand_authored ( ) . map ( |e| & e. author ) ) 
356-         . filter ( |author| !is_maintainer ( author) ) 
357-         . collect :: < Vec < _ > > ( ) ; 
358-     external_contribs. sort ( ) ; 
359-     external_contribs. dedup ( ) ; 
360-     if  !external_contribs. is_empty ( )  { 
361-         out. push_str ( "**Contributors**\n Thank you for your contributions! ❤\n " ) ; 
362-         for  contributor_handle in  external_contribs { 
363-             // retrieve all contributions this author made 
364-             let  mut  contribution_references = entries
365-                 . iter ( ) 
366-                 . filter ( |entry| { 
367-                     entry
368-                         . hand_authored ( ) 
369-                         . map ( |e| e. author . eq_ignore_ascii_case ( contributor_handle. as_str ( ) ) ) 
370-                         . unwrap_or ( false ) 
371-                 } ) 
372-                 . flat_map ( |entry| { 
373-                     entry
374-                         . hand_authored ( ) 
375-                         . unwrap ( ) 
376-                         . references 
377-                         . iter ( ) 
378-                         . map ( to_md_link) 
379-                 } ) 
380-                 . collect :: < Vec < _ > > ( ) ; 
381-             contribution_references. sort ( ) ; 
382-             contribution_references. dedup ( ) ; 
383-             let  contribution_references = contribution_references. as_slice ( ) . join ( ", " ) ; 
384-             out. push_str ( "- @" ) ; 
385-             out. push_str ( contributor_handle) ; 
386-             if  !contribution_references. is_empty ( )  { 
387-                 write ! ( & mut  out,  " ({})" ,  contribution_references) 
388-                     // The `Write` implementation for `String` is infallible, 
389-                     // see https://doc.rust-lang.org/src/alloc/string.rs.html#2815 
390-                     . unwrap ( ) 
391-             } 
392-             out. push ( '\n' ) ; 
393-         } 
394-     } 
471+     render_external_contributors ( entries,  & mut  out) ; 
472+     render_crate_versions ( crate_version_metadata_map,  & mut  out) ; 
395473
396474    ( header,  out) 
397475} 
398476
399477#[ cfg( test) ]  
400478mod  test { 
401479    use  super :: { date_based_release_metadata,  render,  Changelog ,  ChangelogEntries ,  ChangelogEntry } ; 
402-     use  smithy_rs_tool_common:: changelog:: SdkAffected ; 
480+     use  smithy_rs_tool_common:: { 
481+         changelog:: SdkAffected , 
482+         package:: PackageCategory , 
483+         versions_manifest:: { CrateVersion ,  CrateVersionMetadataMap } , 
484+     } ; 
403485    use  time:: OffsetDateTime ; 
404486
405487    fn  render_full ( entries :  & [ ChangelogEntry ] ,  release_header :  & str )  -> String  { 
406-         let  ( header,  body)  = render ( entries,  release_header) ; 
488+         let  ( header,  body)  = render ( entries,  CrateVersionMetadataMap :: new ( ) ,   release_header) ; 
407489        format ! ( "{header}{body}" ) 
408490    } 
409491
@@ -494,6 +576,7 @@ v0.3.0 (January 4th, 2022)
494576Thank you for your contributions! ❤ 
495577- @another-contrib ([smithy-rs#200](https://github.com/awslabs/smithy-rs/issues/200)) 
496578- @external-contrib ([smithy-rs#446](https://github.com/awslabs/smithy-rs/issues/446)) 
579+ 
497580"# 
498581        . trim_start ( ) ; 
499582        pretty_assertions:: assert_str_eq!( smithy_rs_expected,  smithy_rs_rendered) ; 
@@ -518,6 +601,7 @@ v0.1.0 (January 4th, 2022)
518601**Contributors** 
519602Thank you for your contributions! ❤ 
520603- @external-contrib ([smithy-rs#446](https://github.com/awslabs/smithy-rs/issues/446)) 
604+ 
521605"# 
522606        . trim_start ( ) ; 
523607        pretty_assertions:: assert_str_eq!( aws_sdk_expected,  aws_sdk_rust_rendered) ; 
@@ -592,9 +676,69 @@ author = "rcoh"
592676    #[ test]  
593677    fn  test_empty_render ( )  { 
594678        let  smithy_rs = Vec :: < ChangelogEntry > :: new ( ) ; 
595-         let  ( release_title,  release_notes)  = render ( & smithy_rs,  "some header" ) ; 
679+         let  ( release_title,  release_notes)  =
680+             render ( & smithy_rs,  CrateVersionMetadataMap :: new ( ) ,  "some header" ) ; 
596681
597682        assert_eq ! ( release_title,  "some header\n ===========\n " ) ; 
598683        assert_eq ! ( release_notes,  "" ) ; 
599684    } 
685+ 
686+     #[ test]  
687+     fn  test_crate_versions ( )  { 
688+         let  mut  crate_version_metadata_map = CrateVersionMetadataMap :: new ( ) ; 
689+         crate_version_metadata_map. insert ( 
690+             "aws-config" . to_owned ( ) , 
691+             CrateVersion  { 
692+                 category :  PackageCategory :: AwsRuntime , 
693+                 version :  "0.54.1" . to_owned ( ) , 
694+                 source_hash :  "e93380cfbd05e68d39801cbf0113737ede552a5eceb28f4c34b090048d539df9" 
695+                     . to_owned ( ) , 
696+                 model_hash :  None , 
697+             } , 
698+         ) ; 
699+         crate_version_metadata_map. insert ( 
700+             "aws-sdk-accessanalyzer" . to_owned ( ) , 
701+             CrateVersion  { 
702+                 category :  PackageCategory :: AwsSdk , 
703+                 version :  "0.24.0" . to_owned ( ) , 
704+                 source_hash :  "a7728756b41b33d02f68a5865d3456802b7bc3949ec089790bc4e726c0de8539" 
705+                     . to_owned ( ) , 
706+                 model_hash :  Some ( 
707+                     "71f1f130504ebd55396c3166d9441513f97e49b281a5dd420fd7e2429860b41b" . to_owned ( ) , 
708+                 ) , 
709+             } , 
710+         ) ; 
711+         crate_version_metadata_map. insert ( 
712+             "aws-smithy-async" . to_owned ( ) , 
713+             CrateVersion  { 
714+                 category :  PackageCategory :: SmithyRuntime , 
715+                 version :  "0.54.1" . to_owned ( ) , 
716+                 source_hash :  "8ced52afc783cbb0df47ee8b55260b98e9febdc95edd796ed14c43db5199b0a9" 
717+                     . to_owned ( ) , 
718+                 model_hash :  None , 
719+             } , 
720+         ) ; 
721+         let  ( release_title,  release_notes)  = render ( 
722+             & Vec :: < ChangelogEntry > :: new ( ) , 
723+             crate_version_metadata_map, 
724+             "some header" , 
725+         ) ; 
726+ 
727+         assert_eq ! ( release_title,  "some header\n ===========\n " ) ; 
728+         let  expected_body = r#" 
729+ **Crate Versions** 
730+ <details> 
731+ <summary>Click to expand to view crate versions...</summary> 
732+ 
733+ |Crate|Version| 
734+ |-|-| 
735+ |aws-config|0.54.1| 
736+ |aws-sdk-accessanalyzer|0.24.0| 
737+ |aws-smithy-async|0.54.1| 
738+ </details> 
739+ 
740+ "# 
741+         . trim_start ( ) ; 
742+         pretty_assertions:: assert_str_eq!( release_notes,  expected_body) ; 
743+     } 
600744} 
0 commit comments