Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new subcommands to changelogger for supporting file-per-change changelog #3771

Merged
merged 11 commits into from
Jul 31, 2024
Merged
Empty file added .changelog/.keep
Empty file.
35 changes: 33 additions & 2 deletions tools/ci-build/changelogger/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tools/ci-build/changelogger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ opt-level = 0
[dependencies]
anyhow = "1.0.57"
clap = { version = "~3.2.1", features = ["derive"] }
edit = "0.1"
fastrand = "2.1.0"
once_cell = "1.15.0"
ordinal = "0.3.2"
serde = { version = "1", features = ["derive"]}
serde_json = "1"
serde_yaml = "0.9"
smithy-rs-tool-common = { path = "../smithy-rs-tool-common" }
time = { version = "0.3.9", features = ["local-offset"]}
toml = "0.5.8"
Expand Down
14 changes: 14 additions & 0 deletions tools/ci-build/changelogger/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use clap::clap_derive::ArgEnum;
use smithy_rs_tool_common::changelog::{Changelog, HandAuthoredEntry, SdkModelEntry};
use smithy_rs_tool_common::git::Git;
use smithy_rs_tool_common::versions_manifest::VersionsManifest;
use std::fmt::{Display, Formatter};
use std::path::Path;

#[derive(ArgEnum, Copy, Clone, Debug, Eq, PartialEq)]
Expand All @@ -16,6 +17,19 @@ pub enum ChangeSet {
AwsSdk,
}

impl Display for ChangeSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
ChangeSet::SmithyRs => "smithy-rs",
ChangeSet::AwsSdk => "aws-sdk-rust",
}
)
}
}

pub struct ChangelogEntries {
pub aws_sdk_rust: Vec<ChangelogEntry>,
pub smithy_rs: Vec<ChangelogEntry>,
Expand Down
2 changes: 2 additions & 0 deletions tools/ci-build/changelogger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@

pub mod entry;
pub mod init;
pub mod new_entry;
pub mod preview_next;
pub mod render;
pub mod split;
81 changes: 75 additions & 6 deletions tools/ci-build/changelogger/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,49 @@

use anyhow::Result;
use changelogger::init::subcommand_init;
use changelogger::new_entry::subcommand_new_entry;
use changelogger::preview_next::subcommand_preview_next;
use changelogger::render::subcommand_render;
use changelogger::split::subcommand_split;
use clap::Parser;

#[derive(Parser, Debug, Eq, PartialEq)]
#[clap(name = "changelogger", author, version, about)]
pub enum Args {
/// Split SDK changelog entries into a separate file
Split(changelogger::split::SplitArgs),
/// Print to stdout the empty "next" CHANGELOG template
Init(changelogger::init::InitArgs),
/// Create a new changelog entry Markdown file in the `smithy-rs/.changelog` directory
NewEntry(changelogger::new_entry::NewEntryArgs),
/// Render a preview of changelog entries since the last release
PreviewNext(changelogger::preview_next::PreviewNextArgs),
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
/// Render a TOML/JSON changelog into GitHub-flavored Markdown
Render(changelogger::render::RenderArgs),
/// Print to stdout the empty "next" CHANGELOG template.
Init(changelogger::init::InitArgs),
/// Split SDK changelog entries into a separate file
Split(changelogger::split::SplitArgs),
}

fn main() -> Result<()> {
match Args::parse() {
Args::Split(split) => subcommand_split(&split),
Args::Render(render) => subcommand_render(&render),
Args::Init(init) => subcommand_init(&init),
Args::NewEntry(new_entry) => subcommand_new_entry(new_entry),
Args::PreviewNext(preview_next) => subcommand_preview_next(preview_next),
Args::Render(render) => subcommand_render(&render),
Args::Split(split) => subcommand_split(&split),
}
}

#[cfg(test)]
mod tests {
use super::Args;
use changelogger::entry::ChangeSet;
use changelogger::new_entry::NewEntryArgs;
use changelogger::preview_next::PreviewNextArgs;
use changelogger::render::RenderArgs;
use changelogger::split::SplitArgs;
use clap::Parser;
use smithy_rs_tool_common::changelog::{Reference, Target};
use std::path::PathBuf;
use std::str::FromStr;

#[test]
fn args_parsing() {
Expand Down Expand Up @@ -188,5 +200,62 @@ mod tests {
])
.unwrap()
);

assert_eq!(
Args::NewEntry(NewEntryArgs {
applies_to: Some(vec![Target::Client, Target::AwsSdk]),
authors: Some(vec!["external-contrib".to_owned(), "ysaito1001".to_owned()]),
references: Some(vec![
Reference::from_str("smithy-rs#1234").unwrap(),
Reference::from_str("aws-sdk-rust#5678").unwrap()
]),
breaking: false,
new_feature: true,
bug_fix: false,
message: Some("Implement a long-awaited feature for S3".to_owned()),
basename: None,
}),
Args::try_parse_from([
"./changelogger",
"new-entry",
"--applies-to",
"client",
"--applies-to",
"aws-sdk-rust",
"--author",
"external-contrib",
"--author",
"ysaito1001",
"--ref",
"smithy-rs#1234",
"--ref",
"aws-sdk-rust#5678",
"--new-feature",
"--message",
"Implement a long-awaited feature for S3",
])
.unwrap()
);

assert_eq!(
Args::PreviewNext(PreviewNextArgs {
change_set: ChangeSet::SmithyRs
}),
Args::try_parse_from([
"./changelogger",
"preview-next",
"--change-set",
"smithy-rs",
])
.unwrap()
);

assert_eq!(
Args::PreviewNext(PreviewNextArgs {
change_set: ChangeSet::AwsSdk
}),
Args::try_parse_from(["./changelogger", "preview-next", "--change-set", "aws-sdk",])
.unwrap()
);
}
}
145 changes: 145 additions & 0 deletions tools/ci-build/changelogger/src/new_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

use anyhow::Context;
use clap::Parser;
use smithy_rs_tool_common::changelog::{FrontMatter, Markdown, Reference, Target};
use smithy_rs_tool_common::git::find_git_repository_root;
use smithy_rs_tool_common::here;
use std::path::PathBuf;

#[derive(Parser, Debug, Eq, PartialEq)]
pub struct NewEntryArgs {
ysaito1001 marked this conversation as resolved.
Show resolved Hide resolved
/// Target audience for the change (if not provided, user's editor will open for authoring one)
#[clap(long)]
pub applies_to: Option<Vec<Target>>,
/// List of git usernames for the authors of the change (if not provided, user's editor will open for authoring one)
#[clap(long = "author")]
pub authors: Option<Vec<String>>,
/// List of relevant issues and PRs (if not provided, user's editor will open for authoring one)
#[clap(long = "ref")]
pub references: Option<Vec<Reference>>,
/// Whether or not the change contains a breaking change (defaults to false)
#[clap(long, action)]
pub breaking: bool,
/// Whether or not the change implements a new feature (defaults to false)
#[clap(long, action)]
pub new_feature: bool,
/// Whether or not the change fixes a bug (defaults to false)
#[clap(long, action)]
pub bug_fix: bool,
/// The changelog entry message (if not provided, user's editor will open for authoring one)
#[clap(long)]
pub message: Option<String>,
/// Basename of a changelog markdown file (defaults to a random 6-digit basename)
#[clap(long)]
pub basename: Option<PathBuf>,
}

pub fn subcommand_new_entry(args: NewEntryArgs) -> anyhow::Result<()> {
let mut md_full_filename = find_git_repository_root("smithy-rs", ".").context(here!())?;
md_full_filename.push(".changelog");
md_full_filename.push(args.basename.clone().unwrap_or(PathBuf::from(format!(
"{}.md",
fastrand::u32(1_000_000..10_000_000)
))));

let changelog_entry = new_entry(args)?;
std::fs::write(&md_full_filename, &changelog_entry).with_context(|| {
format!(
"failed to write the following changelog entry to {:?}:\n{}",
md_full_filename.as_path(),
changelog_entry
)
})?;

println!(
"\nThe following changelog entry has been written to {:?}:\n{}",
md_full_filename.as_path(),
changelog_entry
);

Ok(())
}

fn new_entry(args: NewEntryArgs) -> anyhow::Result<String> {
let markdown = Markdown {
front_matter: FrontMatter {
applies_to: args.applies_to.unwrap_or_default().into_iter().collect(),
authors: args.authors.unwrap_or_default(),
references: args.references.unwrap_or_default(),
breaking: args.breaking,
new_feature: args.new_feature,
bug_fix: args.bug_fix,
},
message: args.message.unwrap_or_default(),
};
// Due to the inability for `serde_yaml` to output single line array syntax, an array of values
// will be serialized as follows:
//
// key:
// - value1
// - value2
//
// as opposed to:
//
// key: [value1, value2]
//
// This doesn't present practical issues when rendering changelogs. See
// https://github.com/dtolnay/serde-yaml/issues/355
let front_matter = serde_yaml::to_string(&markdown.front_matter)?;
let changelog_entry = format!("---\n{}---\n{}", front_matter, markdown.message);
let changelog_entry = if any_required_field_needs_to_be_filled(&markdown) {
edit::edit(changelog_entry).context("failed while editing changelog entry)")?
} else {
changelog_entry
};

Ok(changelog_entry)
}

fn any_required_field_needs_to_be_filled(markdown: &Markdown) -> bool {
macro_rules! any_empty {
() => { false };
($head:expr $(, $tail:expr)*) => {
$head.is_empty() || any_empty!($($tail),*)
};
}
any_empty!(
&markdown.front_matter.applies_to,
&markdown.front_matter.authors,
&markdown.front_matter.references,
&markdown.message
)
}

#[cfg(test)]
mod tests {
use crate::new_entry::{new_entry, NewEntryArgs};
use smithy_rs_tool_common::changelog::{Reference, Target};
use std::str::FromStr;

#[test]
fn test_new_entry_from_args() {
// make sure `args` populates required fields (so the function
// `any_required_field_needs_to_be_filled` returns true), otherwise an editor would be
// opened during the test execution for human input, causing the test to get struck
let args = NewEntryArgs {
applies_to: Some(vec![Target::Client]),
authors: Some(vec!["ysaito1001".to_owned()]),
references: Some(vec![Reference::from_str("smithy-rs#1234").unwrap()]),
breaking: false,
new_feature: true,
bug_fix: false,
message: Some("Implement a long-awaited feature for S3".to_owned()),
basename: None,
};

let expected = "---\napplies_to:\n- client\nauthors:\n- ysaito1001\nreferences:\n- smithy-rs#1234\nbreaking: false\nnew_feature: true\nbug_fix: false\n---\nImplement a long-awaited feature for S3";
let actual = new_entry(args).unwrap();

assert_eq!(expected, &actual);
}
}
Loading