diff --git a/tools/ci-build/changelogger/src/main.rs b/tools/ci-build/changelogger/src/main.rs index db36023ff35..5e29945e349 100644 --- a/tools/ci-build/changelogger/src/main.rs +++ b/tools/ci-build/changelogger/src/main.rs @@ -65,6 +65,7 @@ mod tests { source_to_truncate: PathBuf::from("fromplace"), changelog_output: PathBuf::from("some-changelog"), release_manifest_output: Some(PathBuf::from("some-manifest")), + current_release_versions_manifest: None, previous_release_versions_manifest: None, date_override: None, smithy_rs_location: None, @@ -97,6 +98,7 @@ mod tests { source_to_truncate: PathBuf::from("fromplace"), changelog_output: PathBuf::from("some-changelog"), release_manifest_output: None, + current_release_versions_manifest: None, previous_release_versions_manifest: None, date_override: None, smithy_rs_location: None, @@ -127,6 +129,7 @@ mod tests { source_to_truncate: PathBuf::from("fromplace"), changelog_output: PathBuf::from("some-changelog"), release_manifest_output: None, + current_release_versions_manifest: None, previous_release_versions_manifest: Some(PathBuf::from("path/to/versions.toml")), date_override: None, smithy_rs_location: None, @@ -148,5 +151,42 @@ mod tests { ]) .unwrap() ); + + assert_eq!( + Args::Render(RenderArgs { + change_set: ChangeSet::AwsSdk, + independent_versioning: true, + source: vec![PathBuf::from("fromplace")], + source_to_truncate: PathBuf::from("fromplace"), + changelog_output: PathBuf::from("some-changelog"), + release_manifest_output: None, + current_release_versions_manifest: Some(PathBuf::from( + "path/to/current/versions.toml" + )), + previous_release_versions_manifest: Some(PathBuf::from( + "path/to/previous/versions.toml" + )), + date_override: None, + smithy_rs_location: None, + }), + Args::try_parse_from([ + "./changelogger", + "render", + "--change-set", + "aws-sdk", + "--independent-versioning", + "--source", + "fromplace", + "--source-to-truncate", + "fromplace", + "--changelog-output", + "some-changelog", + "--current-release-versions-manifest", + "path/to/current/versions.toml", + "--previous-release-versions-manifest", + "path/to/previous/versions.toml" + ]) + .unwrap() + ); } } diff --git a/tools/ci-build/changelogger/src/render.rs b/tools/ci-build/changelogger/src/render.rs index 7526b5fd33e..bb2666b3ef0 100644 --- a/tools/ci-build/changelogger/src/render.rs +++ b/tools/ci-build/changelogger/src/render.rs @@ -13,9 +13,10 @@ use smithy_rs_tool_common::changelog::{ Changelog, HandAuthoredEntry, Reference, SdkModelChangeKind, SdkModelEntry, }; use smithy_rs_tool_common::git::{find_git_repository_root, Git, GitCLI}; +use smithy_rs_tool_common::versions_manifest::{CrateVersionMetadataMap, VersionsManifest}; use std::env; use std::fmt::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use time::OffsetDateTime; pub const EXAMPLE_ENTRY: &str = r#" @@ -67,6 +68,10 @@ pub struct RenderArgs { /// Optional path to output a release manifest file to #[clap(long, action)] pub release_manifest_output: Option, + /// Optional path to the SDK's versions.toml file for the current release. + /// This is used to generate a markdown table showing crate versions. + #[clap(long, action)] + pub current_release_versions_manifest: Option, /// Optional path to the SDK's versions.toml file for the previous release. /// This is used to filter out changelog entries that have `since_commit` information. #[clap(long, action)] @@ -217,6 +222,16 @@ fn indented_message(message: &str) -> String { out } +fn render_table_row(columns: [&str; 2], out: &mut String) { + let mut row = String::new(); + for column in columns { + row.push_str(column); + row.push('|'); + } + write!(out, "{row}").unwrap(); + out.push('\n'); +} + fn load_changelogs(args: &RenderArgs) -> Result { let mut combined = Changelog::new(); for source in &args.source { @@ -233,6 +248,19 @@ fn load_changelogs(args: &RenderArgs) -> Result { Ok(combined) } +fn load_current_crate_version_metadata_map( + current_release_versions_manifest: Option<&Path>, +) -> CrateVersionMetadataMap { + current_release_versions_manifest + .and_then( + |manifest_path| match VersionsManifest::from_file(manifest_path) { + Ok(manifest) => Some(manifest.crates), + Err(_) => None, + }, + ) + .unwrap_or_else(|| CrateVersionMetadataMap::new()) +} + fn update_changelogs( args: &RenderArgs, smithy_rs: &dyn Git, @@ -250,7 +278,13 @@ fn update_changelogs( args.change_set, args.previous_release_versions_manifest.as_deref(), )?; - let (release_header, release_notes) = render(&entries, &release_metadata.title); + let current_crate_version_metadata_map = + load_current_crate_version_metadata_map(args.current_release_versions_manifest.as_deref()); + let (release_header, release_notes) = render( + &entries, + current_crate_version_metadata_map, + &release_metadata.title, + ); if let Some(output_path) = &args.release_manifest_output { let release_manifest = ReleaseManifest { tag_name: release_metadata.tag.clone(), @@ -335,48 +369,86 @@ fn render_external_contributors<'a>(entries: &[ChangelogEntry], out: &mut String .filter_map(|entry| entry.hand_authored().map(|e| &e.author)) .filter(|author| !is_maintainer(author)) .collect::>(); + if external_contribs.is_empty() { + return; + } external_contribs.sort(); external_contribs.dedup(); - if !external_contribs.is_empty() { - out.push_str("**Contributors**\nThank you for your contributions! ❤\n"); - for contributor_handle in external_contribs { - // retrieve all contributions this author made - let mut contribution_references = entries - .iter() - .filter(|entry| { - entry - .hand_authored() - .map(|e| e.author.eq_ignore_ascii_case(contributor_handle.as_str())) - .unwrap_or(false) - }) - .flat_map(|entry| { - entry - .hand_authored() - .unwrap() - .references - .iter() - .map(to_md_link) - }) - .collect::>(); - contribution_references.sort(); - contribution_references.dedup(); - let contribution_references = contribution_references.as_slice().join(", "); - out.push_str("- @"); - out.push_str(contributor_handle); - if !contribution_references.is_empty() { - write!(out, " ({})", contribution_references) - // The `Write` implementation for `String` is infallible, - // see https://doc.rust-lang.org/src/alloc/string.rs.html#2815 + out.push_str("**Contributors**\nThank you for your contributions! ❤\n"); + for contributor_handle in external_contribs { + // retrieve all contributions this author made + let mut contribution_references = entries + .iter() + .filter(|entry| { + entry + .hand_authored() + .map(|e| e.author.eq_ignore_ascii_case(contributor_handle.as_str())) + .unwrap_or(false) + }) + .flat_map(|entry| { + entry + .hand_authored() .unwrap() - } - out.push('\n'); + .references + .iter() + .map(to_md_link) + }) + .collect::>(); + contribution_references.sort(); + contribution_references.dedup(); + let contribution_references = contribution_references.as_slice().join(", "); + out.push_str("- @"); + out.push_str(contributor_handle); + if !contribution_references.is_empty() { + write!(out, " ({})", contribution_references) + // The `Write` implementation for `String` is infallible, + // see https://doc.rust-lang.org/src/alloc/string.rs.html#2815 + .unwrap() } + out.push('\n'); } + out.push('\n'); +} + +fn render_details(summary: &str, body: &str, out: &mut String) { + out.push_str("
"); + out.push('\n'); + write!(out, "{}", summary).unwrap(); + out.push('\n'); + out.push_str(body); + out.push_str("
"); + out.push('\n'); } -/// Convert a list of changelog entries into markdown. +fn render_crate_versions(crate_version_metadata_map: CrateVersionMetadataMap, out: &mut String) { + if crate_version_metadata_map.is_empty() { + // If the map is empty, we choose to not render anything, as opposed to + // rendering the
element with empty contents and a user toggling + // it only to find out there is nothing in it. + return; + } + + out.push_str("**Crate Versions**"); + out.push('\n'); + + let mut table = String::new(); + render_table_row(["Crate", "Version"], &mut table); + render_table_row(["-", "-"], &mut table); + for (crate_name, version_metadata) in &crate_version_metadata_map { + render_table_row([&crate_name, &version_metadata.version], &mut table); + } + + render_details("Click to expand to view crate versions...", &table, out); + out.push('\n'); +} + +/// Convert a list of changelog entries and crate versions into markdown. /// Returns (header, body) -fn render(entries: &[ChangelogEntry], release_header: &str) -> (String, String) { +fn render( + entries: &[ChangelogEntry], + crate_version_metadata_map: CrateVersionMetadataMap, + release_header: &str, +) -> (String, String) { let mut header = String::new(); header.push_str(release_header); header.push('\n'); @@ -395,6 +467,11 @@ fn render(entries: &[ChangelogEntry], release_header: &str) -> (String, String) &mut out, ); render_external_contributors(entries, &mut out); + render_crate_versions(crate_version_metadata_map, &mut out); + + while out.ends_with("\n\n") { + out.pop(); + } (header, out) } @@ -402,11 +479,15 @@ fn render(entries: &[ChangelogEntry], release_header: &str) -> (String, String) #[cfg(test)] mod test { use super::{date_based_release_metadata, render, Changelog, ChangelogEntries, ChangelogEntry}; - use smithy_rs_tool_common::changelog::SdkAffected; + use smithy_rs_tool_common::{ + changelog::SdkAffected, + package::PackageCategory, + versions_manifest::{CrateVersion, CrateVersionMetadataMap}, + }; use time::OffsetDateTime; fn render_full(entries: &[ChangelogEntry], release_header: &str) -> String { - let (header, body) = render(entries, release_header); + let (header, body) = render(entries, CrateVersionMetadataMap::new(), release_header); format!("{header}{body}") } @@ -595,9 +676,67 @@ author = "rcoh" #[test] fn test_empty_render() { let smithy_rs = Vec::::new(); - let (release_title, release_notes) = render(&smithy_rs, "some header"); + let (release_title, release_notes) = + render(&smithy_rs, CrateVersionMetadataMap::new(), "some header"); assert_eq!(release_title, "some header\n===========\n"); assert_eq!(release_notes, ""); } + + #[test] + fn test_crate_versions() { + let mut crate_version_metadata_map = CrateVersionMetadataMap::new(); + crate_version_metadata_map.insert( + "aws-config".to_owned(), + CrateVersion { + category: PackageCategory::AwsRuntime, + version: "0.54.1".to_owned(), + source_hash: "e93380cfbd05e68d39801cbf0113737ede552a5eceb28f4c34b090048d539df9" + .to_owned(), + model_hash: None, + }, + ); + crate_version_metadata_map.insert( + "aws-sdk-accessanalyzer".to_owned(), + CrateVersion { + category: PackageCategory::AwsSdk, + version: "0.24.0".to_owned(), + source_hash: "a7728756b41b33d02f68a5865d3456802b7bc3949ec089790bc4e726c0de8539" + .to_owned(), + model_hash: Some( + "71f1f130504ebd55396c3166d9441513f97e49b281a5dd420fd7e2429860b41b".to_owned(), + ), + }, + ); + crate_version_metadata_map.insert( + "aws-smithy-async".to_owned(), + CrateVersion { + category: PackageCategory::SmithyRuntime, + version: "0.54.1".to_owned(), + source_hash: "8ced52afc783cbb0df47ee8b55260b98e9febdc95edd796ed14c43db5199b0a9" + .to_owned(), + model_hash: None, + }, + ); + let (release_title, release_notes) = render( + &Vec::::new(), + crate_version_metadata_map, + "some header", + ); + + assert_eq!(release_title, "some header\n===========\n"); + let expected_body = r#" +**Crate Versions** +
+Click to expand to view crate versions... +Crate|Version| +-|-| +aws-config|0.54.1| +aws-sdk-accessanalyzer|0.24.0| +aws-smithy-async|0.54.1| +
+"# + .trim_start(); + pretty_assertions::assert_str_eq!(release_notes, expected_body); + } } diff --git a/tools/ci-build/changelogger/tests/e2e_test.rs b/tools/ci-build/changelogger/tests/e2e_test.rs index 07dc64a8391..88c19038089 100644 --- a/tools/ci-build/changelogger/tests/e2e_test.rs +++ b/tools/ci-build/changelogger/tests/e2e_test.rs @@ -45,6 +45,37 @@ const SDK_MODEL_SOURCE_TOML: &str = r#" message = "Some API change" "#; +const VERSIONS_TOML: &str = r#" + smithy_rs_revision = '41ca31b85b4ba8c0ad680fe62a230266cc52cc44' + aws_doc_sdk_examples_revision = '97a177aab8c3d2fef97416cb66e4b4d0da840138' + + [manual_interventions] + crates_to_remove = [] + [crates.aws-config] + category = 'AwsRuntime' + version = '0.54.1' + source_hash = 'e93380cfbd05e68d39801cbf0113737ede552a5eceb28f4c34b090048d539df9' + + [crates.aws-sdk-accessanalyzer] + category = 'AwsSdk' + version = '0.24.0' + source_hash = 'a7728756b41b33d02f68a5865d3456802b7bc3949ec089790bc4e726c0de8539' + model_hash = '71f1f130504ebd55396c3166d9441513f97e49b281a5dd420fd7e2429860b41b' + + [crates.aws-smithy-async] + category = 'SmithyRuntime' + version = '0.54.1' + source_hash = '8ced52afc783cbb0df47ee8b55260b98e9febdc95edd796ed14c43db5199b0a9' + + [release] + tag = 'release-2023-01-26' + + [release.crates] + aws-config = "0.54.1" + aws-sdk-accessanalyzer = '0.24.0' + aws-smithy-async = '0.54.1' +"#; + fn create_fake_repo_root( path: &Path, smithy_rs_version: &str, @@ -98,7 +129,7 @@ fn create_fake_repo_root( } #[test] -fn split_aws_sdk_test() { +fn split_aws_sdk() { let tmp_dir = TempDir::new().unwrap(); let source_path = tmp_dir.path().join("source.toml"); let dest_path = tmp_dir.path().join("dest.toml"); @@ -226,7 +257,7 @@ fn split_aws_sdk_test() { } #[test] -fn render_smithy_rs_test() { +fn render_smithy_rs() { let tmp_dir = TempDir::new().unwrap(); let source_path = tmp_dir.path().join("source.toml"); let dest_path = tmp_dir.path().join("dest.md"); @@ -253,6 +284,7 @@ fn render_smithy_rs_test() { changelog_output: dest_path.clone(), release_manifest_output: Some(tmp_dir.path().into()), date_override: Some(OffsetDateTime::UNIX_EPOCH), + current_release_versions_manifest: None, previous_release_versions_manifest: None, smithy_rs_location: Some(tmp_dir.path().into()), }) @@ -293,13 +325,13 @@ Old entry contents } #[test] -fn render_aws_sdk_test() { +fn render_aws_sdk() { let tmp_dir = TempDir::new().unwrap(); let source1_path = tmp_dir.path().join("source1.toml"); let source2_path = tmp_dir.path().join("source2.toml"); let dest_path = tmp_dir.path().join("dest.md"); let release_manifest_path = tmp_dir.path().join("aws-sdk-rust-release-manifest.json"); - let versions_manifest_path = tmp_dir.path().join("versions.toml"); + let previous_versions_manifest_path = tmp_dir.path().join("versions.toml"); let (release_1_commit, release_2_commit) = create_fake_repo_root(tmp_dir.path(), "0.42.0", "0.12.0"); @@ -322,7 +354,7 @@ fn render_aws_sdk_test() { .unwrap(); fs::write(&release_manifest_path, "overwrite-me").unwrap(); fs::write( - &versions_manifest_path, + &previous_versions_manifest_path, format!( "smithy_rs_revision = '{release_1_commit}' aws_doc_sdk_examples_revision = 'not-relevant' @@ -339,7 +371,8 @@ fn render_aws_sdk_test() { changelog_output: dest_path.clone(), release_manifest_output: Some(tmp_dir.path().into()), date_override: Some(OffsetDateTime::UNIX_EPOCH), - previous_release_versions_manifest: Some(versions_manifest_path), + current_release_versions_manifest: None, + previous_release_versions_manifest: Some(previous_versions_manifest_path), smithy_rs_location: Some(tmp_dir.path().into()), }) .unwrap(); @@ -460,6 +493,7 @@ author = "LukeMathWalker" changelog_output: dest_path.clone(), release_manifest_output: Some(tmp_dir.path().into()), date_override: Some(OffsetDateTime::UNIX_EPOCH), + current_release_versions_manifest: None, previous_release_versions_manifest: None, smithy_rs_location: Some(tmp_dir.path().into()), }) @@ -569,6 +603,7 @@ author = "rcoh" changelog_output: dest_path, release_manifest_output: Some(tmp_dir.path().into()), date_override: Some(OffsetDateTime::UNIX_EPOCH), + current_release_versions_manifest: None, previous_release_versions_manifest: None, smithy_rs_location: Some(tmp_dir.path().into()), }); @@ -582,3 +617,84 @@ author = "rcoh" panic!("This should have been error that aws-sdk-rust has a target entry"); } } + +#[test] +fn render_crate_versions() { + let tmp_dir = TempDir::new().unwrap(); + let source_path = tmp_dir.path().join("source.toml"); + let dest_path = tmp_dir.path().join("dest.md"); + let release_manifest_path = tmp_dir.path().join("smithy-rs-release-manifest.json"); + let current_versions_manifest_path = tmp_dir.path().join("versions.toml"); + + create_fake_repo_root(tmp_dir.path(), "0.54.1", "0.24.0"); + + fs::write(&source_path, SOURCE_TOML).unwrap(); + fs::write( + &dest_path, + format!( + "{}\nv0.54.0 (Some date in the past)\n=========\n\nOld entry contents\n", + USE_UPDATE_CHANGELOGS + ), + ) + .unwrap(); + fs::write(&release_manifest_path, "overwrite-me").unwrap(); + fs::write(¤t_versions_manifest_path, VERSIONS_TOML).unwrap(); + + subcommand_render(&RenderArgs { + change_set: ChangeSet::SmithyRs, + independent_versioning: true, + source: vec![source_path.clone()], + source_to_truncate: source_path.clone(), + changelog_output: dest_path.clone(), + release_manifest_output: Some(tmp_dir.path().into()), + date_override: Some(OffsetDateTime::UNIX_EPOCH), + current_release_versions_manifest: Some(current_versions_manifest_path), + previous_release_versions_manifest: None, + smithy_rs_location: Some(tmp_dir.path().into()), + }) + .unwrap(); + + let source = fs::read_to_string(&source_path).unwrap(); + let dest = fs::read_to_string(&dest_path).unwrap(); + let release_manifest = fs::read_to_string(&release_manifest_path).unwrap(); + + // source file should be empty + pretty_assertions::assert_str_eq!(EXAMPLE_ENTRY.trim(), source); + pretty_assertions::assert_str_eq!( + r#" +January 1st, 1970 +================= +**New this release:** +- (all, [smithy-rs#1234](https://github.com/awslabs/smithy-rs/issues/1234), @another-dev) Another change + +**Contributors** +Thank you for your contributions! ❤ +- @another-dev ([smithy-rs#1234](https://github.com/awslabs/smithy-rs/issues/1234)) + +**Crate Versions** +
+Click to expand to view crate versions... +Crate|Version| +-|-| +aws-config|0.54.1| +aws-sdk-accessanalyzer|0.24.0| +aws-smithy-async|0.54.1| +
+ +v0.54.0 (Some date in the past) +========= + +Old entry contents +"#, + dest + ); + pretty_assertions::assert_str_eq!( + r#"{ + "tagName": "release-1970-01-01", + "name": "January 1st, 1970", + "body": "**New this release:**\n- (all, [smithy-rs#1234](https://github.com/awslabs/smithy-rs/issues/1234), @another-dev) Another change\n\n**Contributors**\nThank you for your contributions! ❤\n- @another-dev ([smithy-rs#1234](https://github.com/awslabs/smithy-rs/issues/1234))\n\n**Crate Versions**\n
\nClick to expand to view crate versions...\nCrate|Version|\n-|-|\naws-config|0.54.1|\naws-sdk-accessanalyzer|0.24.0|\naws-smithy-async|0.54.1|\n
\n", + "prerelease": true +}"#, + release_manifest + ); +}