diff --git a/crates/cli-tools/CHANGELOG.md b/crates/cli-tools/CHANGELOG.md index f2262453..90592e34 100644 --- a/crates/cli-tools/CHANGELOG.md +++ b/crates/cli-tools/CHANGELOG.md @@ -10,6 +10,7 @@ ### Minor +- Add `cargo` and `changelog` modules and features - Handle more errors during platform discovery and `action::PlatformReboot` - Extend `fs::write()` first parameter to set the `OpenOptions` too - Add `error::root_cause_is()` to check the `anyhow::Error` root cause @@ -34,4 +35,4 @@ ## 0.1.0 - + diff --git a/crates/cli-tools/Cargo.lock b/crates/cli-tools/Cargo.lock index f27a40d1..42c5af53 100644 --- a/crates/cli-tools/Cargo.lock +++ b/crates/cli-tools/Cargo.lock @@ -585,6 +585,7 @@ dependencies = [ "indicatif", "log", "rusb", + "semver", "serde", "tokio", "toml", diff --git a/crates/cli-tools/Cargo.toml b/crates/cli-tools/Cargo.toml index 7848bc49..19324a4f 100644 --- a/crates/cli-tools/Cargo.toml +++ b/crates/cli-tools/Cargo.toml @@ -22,6 +22,7 @@ humantime = { version = "2.1.0", default-features = false, optional = true } indicatif = { version = "0.17.8", default-features = false, optional = true } log = { version = "0.4.21", default-features = false } rusb = { version = "0.9.4", default-features = false, optional = true } +semver = { version = "1.0.23", default-features = false, optional = true } serde = { version = "1.0.202", default-features = false, features = ["derive"] } toml = { version = "0.8.13", default-features = false, features = ["display", "parse"] } wasefire-wire = { version = "0.1.1-git", path = "../wire", optional = true } @@ -57,7 +58,7 @@ optional = true [features] action = [ - "dep:cargo_metadata", + "cargo", "dep:clap", "dep:data-encoding", "dep:humantime", @@ -69,6 +70,8 @@ action = [ "dep:wasefire-wire", "tokio/time", ] +cargo = ["dep:cargo_metadata"] +changelog = ["cargo", "dep:clap", "dep:semver"] [lints] clippy.unit-arg = "allow" diff --git a/crates/cli-tools/src/action.rs b/crates/cli-tools/src/action.rs index 4daaeb2e..32a3600a 100644 --- a/crates/cli-tools/src/action.rs +++ b/crates/cli-tools/src/action.rs @@ -17,13 +17,13 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::{bail, ensure, Result}; -use cargo_metadata::{Metadata, MetadataCommand}; use clap::{ValueEnum, ValueHint}; use rusb::GlobalContext; use tokio::process::Command; use wasefire_protocol::{self as service, applet, Connection, ConnectionExt}; use wasefire_wire::{self as wire, Yoke}; +use crate::cargo::metadata; use crate::error::root_cause_is; use crate::{cmd, fs}; @@ -633,15 +633,6 @@ pub async fn optimize_wasm(applet: impl AsRef, opt_level: Option Ok(()) } -async fn metadata(dir: impl Into) -> Result { - let dir = dir.into(); - let metadata = - tokio::task::spawn_blocking(|| MetadataCommand::new().current_dir(dir).no_deps().exec()) - .await??; - ensure!(metadata.packages.len() == 1, "not exactly one package"); - Ok(metadata) -} - fn wasefire_feature( package: &cargo_metadata::Package, feature: &str, cargo: &mut Command, ) -> Result<()> { diff --git a/crates/cli-tools/src/cargo.rs b/crates/cli-tools/src/cargo.rs new file mode 100644 index 00000000..3e5f81e5 --- /dev/null +++ b/crates/cli-tools/src/cargo.rs @@ -0,0 +1,27 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::PathBuf; + +use anyhow::{ensure, Result}; +use cargo_metadata::{Metadata, MetadataCommand}; + +pub async fn metadata(dir: impl Into) -> Result { + let dir = dir.into(); + let metadata = + tokio::task::spawn_blocking(|| MetadataCommand::new().current_dir(dir).no_deps().exec()) + .await??; + ensure!(metadata.packages.len() == 1, "not exactly one package"); + Ok(metadata) +} diff --git a/crates/cli-tools/src/changelog.rs b/crates/cli-tools/src/changelog.rs new file mode 100644 index 00000000..5b717dd3 --- /dev/null +++ b/crates/cli-tools/src/changelog.rs @@ -0,0 +1,720 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helpers for changelog files. +//! +//! See for a +//! description of the changelog format. + +use core::str; +use std::collections::BTreeMap; +use std::fmt::Display; +use std::io::BufRead; + +use anyhow::{anyhow, bail, ensure, Context, Result}; +use clap::ValueEnum; +use semver::Version; +use tokio::process::Command; + +use crate::cargo::metadata; +use crate::{cmd, fs}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum Severity { + Major, + Minor, + Patch, +} + +impl Display for Severity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Major => write!(f, "Major"), + Self::Minor => write!(f, "Minor"), + Self::Patch => write!(f, "Patch"), + } + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct Changelog { + releases: Vec, + skip_counter: u32, +} + +impl Changelog { + async fn read_file(path: &str) -> Result { + Self::parse(&String::from_utf8(fs::read(path).await?)?) + } + + /// Parses and validates a changelog. + fn parse(input: &str) -> Result { + let mut releases: Vec = Vec::new(); + let mut parser = Parser::new(input.lines()); + parser.read_exact("# Changelog")?; + parser.read_empty()?; + parser.advance()?; + loop { + let version = (parser.buffer.strip_prefix("## ")) + .with_context(|| anyhow!("Expected release {parser}"))?; + let version = + Version::parse(version).with_context(|| anyhow!("Parsing version {parser}"))?; + ensure!(version.build.is_empty(), "Unexpected build metadata {parser}"); + match releases.last() { + Some(prev) => { + ensure!(version.pre.is_empty(), "Unexpected prerelease {parser}"); + let severity = *prev.contents.first_key_value().unwrap().0; + ensure_conform(&version, severity, &prev.version)?; + } + None => { + let pre = version.pre.as_str(); + ensure!(matches!(pre, "" | "git"), "Invalid prerelease {pre:?} {parser}"); + } + } + parser.read_empty()?; + parser.advance()?; + let mut contents = BTreeMap::new(); + if matches!(version, Version { major: 0, minor: 1, patch: 0, .. }) { + releases.push(Release { version, contents }); + break; + } + while let Some(severity) = parser.buffer.strip_prefix("### ") { + let severity = match severity { + "Major" => Severity::Major, + "Minor" => Severity::Minor, + "Patch" => Severity::Patch, + _ => bail!("Invalid severity {severity:?} {parser}"), + }; + if let Some(&prev) = contents.last_key_value().map(|(x, _)| x) { + ensure!(prev < severity, "Out of order severity {parser}"); + } + parser.read_empty()?; + let mut descriptions = Vec::new(); + while parser.read_empty().is_err() { + if parser.buffer.starts_with("- ") { + descriptions.push(parser.buffer.to_string()); + } else if parser.buffer.starts_with(" ") { + let description = descriptions + .last_mut() + .with_context(|| anyhow!("Invalid continuation {parser}"))?; + description.push('\n'); + description.push_str(parser.buffer); + } else { + bail!("Invalid description {parser}"); + } + ensure!( + !descriptions.last_mut().unwrap().ends_with("."), + "Description ends with dot {parser}" + ); + } + assert!(contents.insert(severity, descriptions).is_none()); + parser.advance()?; + } + ensure!(!contents.is_empty(), "Release {version} is empty"); + releases.push(Release { version, contents }); + } + ensure!(!releases.is_empty(), "Changelog has no releases"); + let skip_counter = parser + .buffer + .strip_prefix("") + .with_context(|| anyhow!("Invalid skip counter suffix {parser}"))? + .parse() + .with_context(|| anyhow!("Invalid skip counter {parser}"))?; + parser.done()?; + let result = Changelog { releases, skip_counter }; + assert_eq!(format!("{result}"), input); + Ok(result) + } + + async fn validate_cargo_toml(&self, path: &str) -> Result<()> { + let metadata = metadata(path).await?; + ensure!( + self.releases.first().unwrap().version == metadata.packages[0].version, + "Version mismatch between Cargo.toml and CHANGELOG.md for {path}" + ); + Ok(()) + } +} + +impl Display for Changelog { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Changelog { releases, skip_counter } = self; + writeln!(f, "# Changelog\n")?; + for release in releases { + write!(f, "{release}")?; + } + writeln!(f, "") + } +} + +struct Parser<'a, I: Iterator> { + count: usize, + buffer: &'a str, + lines: I, +} + +impl<'a, I: Iterator> Display for Parser<'a, I> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let line = if f.alternate() { "Line" } else { "line" }; + let count = self.count; + write!(f, "{line} {count}") + } +} + +impl<'a, I: Iterator> Parser<'a, I> { + fn new(lines: I) -> Self { + Parser { count: 0, buffer: "", lines } + } + + fn done(mut self) -> Result<()> { + Ok(ensure!(self.lines.next().is_none(), "Expected end of file after {self}")) + } + + fn advance(&mut self) -> Result<&'a str> { + self.buffer = + self.lines.next().with_context(|| anyhow!("Unexpected end of file after {self}"))?; + self.count += 1; + Ok(self.buffer) + } + + fn read_exact(&mut self, line: &str) -> Result<()> { + self.advance()?; + ensure!(self.buffer == line, "{self:#} should be {line:?}"); + Ok(()) + } + + fn read_empty(&mut self) -> Result<()> { + self.advance()?; + ensure!(self.buffer.is_empty(), "{self:#} should be empty"); + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq)] +struct Release { + version: Version, + contents: BTreeMap>, +} + +impl Display for Release { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "## {}\n", self.version)?; + for (severity, descriptions) in &self.contents { + writeln!(f, "### {severity}\n")?; + for description in descriptions { + writeln!(f, "{description}")?; + } + writeln!(f)?; + } + Ok(()) + } +} + +fn ensure_conform(old: &Version, severity: Severity, new: &Version) -> Result<()> { + let effective = match (new.major, severity) { + (0, Severity::Major) => Severity::Minor, + (0, _) => Severity::Patch, + (_, x) => x, + }; + fn aux(threshold: Severity, actual: Severity, old: u64) -> u64 { + match actual.cmp(&threshold) { + std::cmp::Ordering::Less => 0, + std::cmp::Ordering::Equal => old + 1, + std::cmp::Ordering::Greater => old, + } + } + let mut expected = new.clone(); + expected.major = aux(Severity::Major, effective, old.major); + expected.minor = aux(Severity::Minor, effective, old.minor); + expected.patch = aux(Severity::Patch, effective, old.patch); + ensure!( + *new == expected, + "Release {new} should be {expected} due to {} bump from {old}", + severity.to_possible_value().unwrap().get_name() + ); + Ok(()) +} + +/// Validates changelog files for all crates. +pub async fn execute_ci() -> Result<()> { + let paths = cmd::output(Command::new("git").args(["ls-files", "*/CHANGELOG.md"])).await?; + for path in paths.stdout.lines() { + let path = path?; + let changelog = Changelog::read_file(&path).await?; + changelog.validate_cargo_toml(path.strip_suffix("/CHANGELOG.md").unwrap()).await?; + } + Ok(()) +} + +/// Updates a changelog file and changelog files of dependencies. +pub async fn execute_change(path: &str, _severity: &Severity, _description: &str) -> Result<()> { + ensure!(fs::exists(path).await, "Crate does not exist: {path}"); + + let _changelog = Changelog::read_file(&format!("{path}/CHANGELOG.md")).await?; + + todo!("Implement changelog updates"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_changelog_success() { + let changelog = r"# Changelog + +## 0.3.0 + +### Major + +- major update 1 +- major update 2 + +### Minor + +- minor update 1 +- minor update 2 + +### Patch + +- patch update 1 +- patch update 2 + +## 0.2.0 + +### Major + +- major update 1 +- major update 2 + +### Minor + +- minor update 1 +- minor update 2 + +### Patch + +- patch update 1 +- patch update 2 + +## 0.1.0 + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap(), + Changelog { + releases: vec![ + Release { + version: Version::parse("0.3.0").unwrap(), + contents: BTreeMap::from([ + ( + Severity::Major, + vec![ + "- major update 1".to_string(), + "- major update 2".to_string() + ] + ), + ( + Severity::Minor, + vec![ + "- minor update 1".to_string(), + "- minor update 2".to_string() + ] + ), + ( + Severity::Patch, + vec![ + "- patch update 1".to_string(), + "- patch update 2".to_string() + ] + ) + ]), + }, + Release { + version: Version::parse("0.2.0").unwrap(), + contents: BTreeMap::from([ + ( + Severity::Major, + vec![ + "- major update 1".to_string(), + "- major update 2".to_string() + ] + ), + ( + Severity::Minor, + vec![ + "- minor update 1".to_string(), + "- minor update 2".to_string() + ] + ), + ( + Severity::Patch, + vec![ + "- patch update 1".to_string(), + "- patch update 2".to_string() + ] + ) + ]), + }, + Release { + version: Version::parse("0.1.0").unwrap(), + contents: BTreeMap::new(), + } + ], + skip_counter: 0, + } + ); + } + + #[test] + fn write_changelog_success() { + let changelog = r"# Changelog + +## 0.3.0 + +### Major + +- major update 1 +- major update 2 + +### Minor + +- minor update 1 +- minor update 2 + +### Patch + +- patch update 1 +- patch update 2 + +## 0.2.0 + +### Major + +- major update 1 +- major update 2 + +### Minor + +- minor update 1 +- minor update 2 + +### Patch + +- patch update 1 +- patch update 2 + +## 0.1.0 + + +"; + + assert_eq!(format!("{}", Changelog::parse(changelog).unwrap()), changelog); + } + + #[test] + fn parse_changelog_with_missing_severity_success() { + let changelog = r"# Changelog + +## 0.2.0 + +### Major + +- major update 1 +- major update 2 + +## 0.1.2 + +### Minor + +- minor update 1 +- minor update 2 + +## 0.1.1 + +### Patch + +- patch update 1 +- patch update 2 + +## 0.1.0 + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap(), + Changelog { + releases: vec![ + Release { + version: Version::parse("0.2.0").unwrap(), + contents: BTreeMap::from([( + Severity::Major, + vec!["- major update 1".to_string(), "- major update 2".to_string()] + )]), + }, + Release { + version: Version::parse("0.1.2").unwrap(), + contents: BTreeMap::from([( + Severity::Minor, + vec!["- minor update 1".to_string(), "- minor update 2".to_string()] + )]), + }, + Release { + version: Version::parse("0.1.1").unwrap(), + contents: BTreeMap::from([( + Severity::Patch, + vec!["- patch update 1".to_string(), "- patch update 2".to_string()] + )]), + }, + Release { + version: Version::parse("0.1.0").unwrap(), + contents: BTreeMap::new(), + } + ], + skip_counter: 0, + } + ); + } + + #[test] + fn parse_changelog_handles_multi_line_description() { + let changelog = r"# Changelog + +## 0.2.0 + +### Major + +- short 1 +- my long description + that spans many lines +- short 2 + +## 0.1.0 + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap(), + Changelog { + releases: vec![ + Release { + version: Version::parse("0.2.0").unwrap(), + contents: BTreeMap::from([( + Severity::Major, + vec![ + "- short 1".to_string(), + "- my long description\n that spans many lines".to_string(), + "- short 2".to_string() + ] + )]), + }, + Release { + version: Version::parse("0.1.0").unwrap(), + contents: BTreeMap::new(), + } + ], + skip_counter: 0, + } + ); + } + + #[test] + fn parse_changelog_handles_description_must_not_end_with_period() { + let changelog = r"# Changelog + +## 0.2.0 + +### Major + +- short 1. + +## 0.1.0 + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap_err().to_string(), + "Description ends with dot line 7" + ); + } + + #[test] + fn parse_changelog_removes_prefix() { + let changelog = r"# Changelog + +## 0.1.0 + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap(), + Changelog { + releases: vec![Release { + version: Version::parse("0.1.0").unwrap(), + contents: BTreeMap::new(), + }], + skip_counter: 0, + } + ); + } + + #[test] + fn parse_changelog_handles_skip_counter_at_end() { + let changelog = r"# Changelog + +## 0.1.0 + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap(), + Changelog { + releases: vec![Release { + version: Version::parse("0.1.0").unwrap(), + contents: BTreeMap::new(), + }], + skip_counter: 5, + } + ); + } + + #[test] + fn parse_changelog_requires_skip_counter_at_end() { + let changelog = r"# Changelog + +## 0.1.0 + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap_err().to_string(), + "Unexpected end of file after line 4" + ); + } + + #[test] + fn parse_changelog_must_start_with_h1_changelog() { + let changelog = r" +## 0.1.0"; + + assert_eq!( + Changelog::parse(changelog).unwrap_err().to_string(), + "Line 1 should be \"# Changelog\"" + ); + } + + #[test] + fn parse_changelog_versions_must_be_in_order() { + let changelog = r"# Changelog + +## 0.1.1 + +### Major + +- A change + +## 0.2.0 + +### Major + +- A change + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap_err().to_string(), + "Release 0.1.1 should be 0.3.0 due to major bump from 0.2.0" + ); + } + + #[test] + fn parse_changelog_versions_requires_at_least_one_release() { + let changelog = r"# Changelog + + +"; + + assert_eq!(Changelog::parse(changelog).unwrap_err().to_string(), "Expected release line 3"); + } + + #[test] + fn parse_changelog_versions_last_version_must_be_0_1_0() { + let changelog = r"# Changelog + +## 0.2.0 + +### Major + +- A change + + +"; + + assert_eq!(Changelog::parse(changelog).unwrap_err().to_string(), "Expected release line 9"); + } + + #[test] + fn parse_changelog_versions_last_version_must_be_empty() { + let changelog = r"# Changelog + +## 0.1.0 + +### Major + +- A change + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap_err().to_string(), + "Invalid skip counter prefix line 5" + ); + } + + #[test] + fn parse_changelog_versions_only_first_can_be_prerelease() { + let changelog = r"# Changelog + +## 0.6.0-git + +### Major + +- A change + +## 0.5.0-git + +### Major + +- A change + + +"; + + assert_eq!( + Changelog::parse(changelog).unwrap_err().to_string(), + "Unexpected prerelease line 9" + ); + } +} diff --git a/crates/cli-tools/src/fs.rs b/crates/cli-tools/src/fs.rs index 0b67c163..932d5c1e 100644 --- a/crates/cli-tools/src/fs.rs +++ b/crates/cli-tools/src/fs.rs @@ -22,7 +22,7 @@ use std::path::{Component, Path, PathBuf}; use anyhow::{Context, Result}; use serde::de::DeserializeOwned; use serde::Serialize; -use tokio::fs::{File, OpenOptions}; +use tokio::fs::{File, OpenOptions, ReadDir}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; pub async fn canonicalize(path: impl AsRef) -> Result { @@ -120,6 +120,12 @@ pub async fn read(path: impl AsRef) -> Result> { tokio::fs::read(path.as_ref()).await.with_context(|| format!("reading {name}")) } +pub async fn read_dir(path: impl AsRef) -> Result { + let dir = path.as_ref().display(); + debug!("walk {dir:?}"); + tokio::fs::read_dir(path.as_ref()).await.with_context(|| format!("walking {dir}")) +} + pub async fn read_toml(path: impl AsRef) -> Result { let name = path.as_ref().display(); let contents = read(path.as_ref()).await?; diff --git a/crates/cli-tools/src/lib.rs b/crates/cli-tools/src/lib.rs index 957676ff..5a419180 100644 --- a/crates/cli-tools/src/lib.rs +++ b/crates/cli-tools/src/lib.rs @@ -34,6 +34,10 @@ macro_rules! debug { #[cfg(feature = "action")] pub mod action; +#[cfg(feature = "cargo")] +pub mod cargo; +#[cfg(feature = "changelog")] +pub mod changelog; pub mod cmd; pub mod error; pub mod fs; diff --git a/crates/cli-tools/test.sh b/crates/cli-tools/test.sh index 85089814..ecf9cfb1 100755 --- a/crates/cli-tools/test.sh +++ b/crates/cli-tools/test.sh @@ -19,5 +19,8 @@ set -e test_helper +cargo test --lib --all-features cargo test --lib --features=action +cargo test --lib --features=cargo +cargo test --lib --features=changelog cargo test --lib diff --git a/crates/xtask/Cargo.lock b/crates/xtask/Cargo.lock index 015177b1..a65834ec 100644 --- a/crates/xtask/Cargo.lock +++ b/crates/xtask/Cargo.lock @@ -1853,6 +1853,7 @@ dependencies = [ "indicatif", "log", "rusb", + "semver", "serde", "tokio", "toml", diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 61a3368e..773aedba 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -16,7 +16,7 @@ rustc-demangle = "0.1.24" serde = { version = "1.0.202", features = ["derive"] } stack-sizes = "0.5.0" tokio = { version = "1.40.0", features = ["full"] } -wasefire-cli-tools = { path = "../cli-tools", features = ["action"] } +wasefire-cli-tools = { path = "../cli-tools", features = ["action", "changelog"] } wasefire-error = { version = "0.1.2-git", path = "../error", features = ["std"] } [lints] diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index f3e4ea6e..5f4d00f9 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -28,7 +28,7 @@ use rustc_demangle::demangle; use tokio::process::Command; use tokio::sync::OnceCell; use wasefire_cli_tools::error::root_cause_is; -use wasefire_cli_tools::{action, cmd, fs}; +use wasefire_cli_tools::{action, changelog, cmd, fs}; mod footprint; mod lazy; @@ -104,6 +104,9 @@ enum MainCommand { /// Ensures review can be done in printed form. Textreview, + + /// Performs a changelog operation. + Changelog(Changelog), } #[derive(clap::Args)] @@ -254,6 +257,30 @@ struct Wait { options: action::ConnectionOptions, } +#[derive(clap::Args)] +struct Changelog { + #[clap(subcommand)] + command: ChangelogCommand, +} + +#[derive(clap::Subcommand)] +enum ChangelogCommand { + /// Validates all changelogs. + Ci, + + /// Logs a crate change. + Change { + /// Path to the changed crate. + path: String, + + /// Severity of the change. + severity: changelog::Severity, + + /// One-line description of the change. + description: String, + }, +} + impl Flags { async fn execute(self) -> Result<()> { match self.command { @@ -263,6 +290,12 @@ impl Flags { MainCommand::WaitPlatform(wait) => wait.execute(false).await, MainCommand::Footprint { output } => footprint::compare(&output).await, MainCommand::Textreview => textreview::execute().await, + MainCommand::Changelog(subcommand) => match subcommand.command { + ChangelogCommand::Ci => changelog::execute_ci().await, + ChangelogCommand::Change { path, severity, description } => { + changelog::execute_change(&path, &severity, &description).await + } + }, } } } diff --git a/scripts/ci-changelog.sh b/scripts/ci-changelog.sh index 19e5c857..35dc6987 100755 --- a/scripts/ci-changelog.sh +++ b/scripts/ci-changelog.sh @@ -19,6 +19,8 @@ set -e # This script checks that Cargo.toml and CHANGELOG.md files are correct. +x cargo xtask changelog ci + # All source files should be under /src/. In praticular, /build.rs should be under /src/build.rs and # package.build set to point to that path. INCLUDE='["/LICENSE", "/src/"]'