From 98b0d800713066dd34e753bc2bdc2a8152c233c7 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 12 Oct 2021 12:43:59 -0700 Subject: [PATCH 1/6] Implement initial tool for publishing to crates.io --- tools/publisher/Cargo.toml | 20 + tools/publisher/README.md | 1 + tools/publisher/src/cargo.rs | 86 ++++ tools/publisher/src/fs.rs | 52 +++ tools/publisher/src/main.rs | 53 +++ tools/publisher/src/package.rs | 404 ++++++++++++++++++ tools/publisher/src/repo.rs | 50 +++ tools/publisher/src/sort.rs | 133 ++++++ .../publisher/src/subcommand/fix_manifests.rs | 185 ++++++++ tools/publisher/src/subcommand/mod.rs | 7 + tools/publisher/src/subcommand/publish.rs | 90 ++++ 11 files changed, 1081 insertions(+) create mode 100644 tools/publisher/Cargo.toml create mode 100644 tools/publisher/README.md create mode 100644 tools/publisher/src/cargo.rs create mode 100644 tools/publisher/src/fs.rs create mode 100644 tools/publisher/src/main.rs create mode 100644 tools/publisher/src/package.rs create mode 100644 tools/publisher/src/repo.rs create mode 100644 tools/publisher/src/sort.rs create mode 100644 tools/publisher/src/subcommand/fix_manifests.rs create mode 100644 tools/publisher/src/subcommand/mod.rs create mode 100644 tools/publisher/src/subcommand/publish.rs diff --git a/tools/publisher/Cargo.toml b/tools/publisher/Cargo.toml new file mode 100644 index 000000000000..225941bfd71e --- /dev/null +++ b/tools/publisher/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "publisher" +version = "0.1.0" +authors = ["AWS Rust SDK Team "] +description = "Tool used to publish the AWS SDK to crates.io" +edition = "2018" +license = "Apache-2.0" +publish = false + +[dependencies] +anyhow = "1.0" +cargo_toml = "0.10.1" +clap = "2.33" +dialoguer = "0.8" +semver = "1.0" +thiserror = "1.0" +tokio = { version = "1.12", features = ["full"] } +toml = "0.5.8" +tracing = "0.1.29" +tracing-subscriber = "0.2.25" \ No newline at end of file diff --git a/tools/publisher/README.md b/tools/publisher/README.md new file mode 100644 index 000000000000..b67da0626a09 --- /dev/null +++ b/tools/publisher/README.md @@ -0,0 +1 @@ +This is a tool that the SDK developer team uses to publish the AWS SDK to crates.io. \ No newline at end of file diff --git a/tools/publisher/src/cargo.rs b/tools/publisher/src/cargo.rs new file mode 100644 index 000000000000..a21b54dd4300 --- /dev/null +++ b/tools/publisher/src/cargo.rs @@ -0,0 +1,86 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Module for interacting with Cargo. + +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +macro_rules! cmd { + [ $( $x:expr ),* ] => { + { + let mut cmd = Cmd::new(); + $(cmd.push($x);)* + cmd + } + }; +} + +/// Confirms that cargo exists on the path. +pub async fn confirm_installed_on_path() -> Result<()> { + cmd!["cargo", "--version"] + .spawn() + .await + .context("cargo is not installed on the PATH")?; + Ok(()) +} + +/// Returns a `Cmd` that, when spawned, will asynchronously run `cargo publish` in the given crate path. +pub fn publish_task(crate_path: &Path) -> Cmd { + cmd!["cargo", "publish"].working_dir(crate_path) +} + +#[derive(Default)] +pub struct Cmd { + parts: Vec, + working_dir: Option, +} + +impl Cmd { + fn new() -> Cmd { + Default::default() + } + + fn push(&mut self, part: impl Into) { + self.parts.push(part.into()); + } + + fn working_dir(mut self, working_dir: impl AsRef) -> Self { + self.working_dir = Some(working_dir.as_ref().into()); + self + } + + /// Returns a plan string that can be output to the user to describe the command. + pub fn plan(&self) -> String { + let mut plan = String::new(); + if let Some(working_dir) = &self.working_dir { + plan.push_str(&format!("[in {:?}]: ", working_dir)); + } + plan.push_str(&self.parts.join(" ").to_string()); + plan + } + + /// Runs the command asynchronously. + pub async fn spawn(mut self) -> Result { + let working_dir = self + .working_dir + .take() + .unwrap_or_else(|| std::env::current_dir().unwrap()); + let mut command: Command = self.into(); + tokio::task::spawn_blocking(move || Ok(command.current_dir(working_dir).output()?)).await? + } +} + +impl From for Command { + fn from(cmd: Cmd) -> Self { + assert!(cmd.parts.len() > 0); + let mut command = Command::new(&cmd.parts[0]); + for i in 1..cmd.parts.len() { + command.arg(&cmd.parts[i]); + } + command + } +} diff --git a/tools/publisher/src/fs.rs b/tools/publisher/src/fs.rs new file mode 100644 index 000000000000..1bcb53c5d5e0 --- /dev/null +++ b/tools/publisher/src/fs.rs @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use anyhow::{Context, Result}; +use std::path::Path; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +/// Abstraction of the filesystem to allow for more tests to be added in the future. +#[derive(Clone, Debug)] +pub enum Fs { + Real, +} + +impl Fs { + /// Reads entire file into `Vec` + pub async fn read_file(&self, path: impl AsRef) -> Result> { + match self { + Fs::Real => tokio_read_file(path.as_ref()).await, + } + } + + /// Writes an entire file from a `&[u8]` + pub async fn write_file(&self, path: impl AsRef, contents: &[u8]) -> Result<()> { + match self { + Fs::Real => tokio_write_file(path.as_ref(), contents).await, + } + } +} + +async fn tokio_read_file(path: &Path) -> Result> { + let mut contents = Vec::new(); + let mut file = File::open(path) + .await + .with_context(|| format!("failed to open {:?}", path))?; + file.read_to_end(&mut contents) + .await + .with_context(|| format!("failed to read {:?}", path))?; + Ok(contents) +} + +async fn tokio_write_file(path: &Path, contents: &[u8]) -> Result<()> { + let mut file = File::create(path) + .await + .with_context(|| format!("failed to create {:?}", path))?; + file.write_all(contents) + .await + .with_context(|| format!("failed to write {:?}", path))?; + Ok(()) +} diff --git a/tools/publisher/src/main.rs b/tools/publisher/src/main.rs new file mode 100644 index 000000000000..dfc759890968 --- /dev/null +++ b/tools/publisher/src/main.rs @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use crate::subcommand::fix_manifests::subcommand_fix_manifests; +use crate::subcommand::publish::subcommand_publish; +use anyhow::Result; +use clap::{crate_authors, crate_description, crate_name, crate_version}; + +mod cargo; +mod fs; +mod package; +mod repo; +mod sort; +mod subcommand; + +pub const REPO_NAME: &'static str = "aws-sdk-rust"; +pub const REPO_CRATE_PATH: &'static str = "sdk"; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + std::env::var("RUST_LOG").unwrap_or_else(|_| "error,publisher=info".to_owned()), + ) + .init(); + + let matches = clap_app().get_matches(); + if let Some(_matches) = matches.subcommand_matches("publish") { + subcommand_publish().await?; + } else if let Some(_matches) = matches.subcommand_matches("fix-manifests") { + subcommand_fix_manifests().await?; + } else { + clap_app().print_long_help().unwrap(); + } + Ok(()) +} + +fn clap_app() -> clap::App<'static, 'static> { + clap::App::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()) + // In the future, there may be another subcommand for yanking + .subcommand( + clap::SubCommand::with_name("fix-manifests") + .about("fixes path dependencies in manifests to also have version numbers"), + ) + .subcommand( + clap::SubCommand::with_name("publish").about("publishes the AWS SDK to crates.io"), + ) +} diff --git a/tools/publisher/src/package.rs b/tools/publisher/src/package.rs new file mode 100644 index 000000000000..d09b0c7b3ee9 --- /dev/null +++ b/tools/publisher/src/package.rs @@ -0,0 +1,404 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Packages, package discovery, and package batching logic. + +use crate::fs::Fs; +use crate::sort::dependency_order; +use anyhow::{Context, Result}; +use cargo_toml::{Dependency, DepsSet, Manifest}; +use semver::Version; +use std::collections::{BTreeMap, BTreeSet}; +use std::error::Error as StdError; +use std::path::{Path, PathBuf}; +use tokio::fs; + +/// Information required to identify a package (crate). +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct PackageHandle { + pub name: String, + pub version: Version, +} + +impl PackageHandle { + pub fn new(name: impl Into, version: Version) -> Self { + Self { + name: name.into(), + version, + } + } +} + +/// Represents a crate (called Package since crate is a reserved word). +#[derive(Debug)] +pub struct Package { + pub handle: PackageHandle, + pub crate_path: PathBuf, + pub manifest_path: PathBuf, + pub local_dependencies: BTreeSet, +} + +impl Package { + pub fn new( + handle: PackageHandle, + manifest_path: impl Into, + local_dependencies: BTreeSet, + ) -> Self { + let manifest_path = manifest_path.into(); + Self { + handle, + crate_path: manifest_path.parent().unwrap().into(), + manifest_path, + local_dependencies, + } + } + + /// Returns `true` if this package depends on `other` + pub fn locally_depends_on(&self, other: &PackageHandle) -> bool { + self.local_dependencies.contains(other) + } +} + +/// Batch of packages. +pub type PackageBatch = Vec; + +/// Discovers publishable packages in the given directory and returns them as +/// batches that can be published in order. +pub async fn discover_package_batches(fs: Fs, path: impl AsRef) -> Result> { + let manifest_paths = discover_package_manifests(path).await?; + let packages = read_packages(fs, manifest_paths).await?; + validate_packages(&packages)?; + batch_packages(packages) +} + +type BoxError = Box; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid manifest {0:?}")] + InvalidManifest(PathBuf), + #[error( + "Invalid crate version {1} in {0:?}: {2}. NOTE: All local dependencies \ + must have complete version numbers rather than version requirements." + )] + InvalidCrateVersion(PathBuf, String, BoxError), + #[error("{0:?} missing version in dependency {1}")] + MissingVersion(PathBuf, String), + #[error("crate {0} has multiple versions: {1} and {2}")] + MultipleVersions(String, Version, Version), +} + +/// Discovers all Cargo.toml files under the given path with a depth limit of 1. +pub async fn discover_package_manifests(path: impl AsRef) -> Result> { + let mut manifests = Vec::new(); + let mut read_dir = fs::read_dir(path).await?; + while let Some(entry) = read_dir.next_entry().await? { + let package_path = entry.path(); + if package_path.is_dir() { + let manifest_path = package_path.join("Cargo.toml"); + if manifest_path.exists() { + manifests.push(manifest_path); + } + } + } + Ok(manifests) +} + +/// Parses a semver version number and adds additional error context when parsing fails. +pub fn parse_version(manifest_path: &Path, version: &str) -> Result { + Version::parse(&version) + .map_err(|err| Error::InvalidCrateVersion(manifest_path.into(), version.into(), err.into())) +} + +fn read_dependencies(path: &Path, dependencies: &DepsSet) -> Result> { + let mut result = Vec::new(); + for (name, metadata) in dependencies { + match metadata { + Dependency::Simple(_) => {} + Dependency::Detailed(detailed) => { + if detailed.path.is_some() { + let version = detailed + .version + .as_ref() + .map(|version| parse_version(path, &version)) + .ok_or_else(|| Error::MissingVersion(path.into(), name.into()))??; + result.push(PackageHandle::new(name, version)); + } + } + } + } + Ok(result) +} + +fn read_package(path: &Path, manifest_bytes: &[u8]) -> Result { + let manifest = Manifest::from_slice(manifest_bytes) + .with_context(|| format!("failed to load package manifest for {:?}", path))?; + let package = manifest + .package + .ok_or_else(|| Error::InvalidManifest(path.into())) + .context("crate manifest doesn't have a `[package]` section")?; + let name = package.name; + let version = parse_version(path, &package.version)?; + let handle = PackageHandle { name, version }; + + let mut local_dependencies = BTreeSet::new(); + local_dependencies.extend(read_dependencies(path, &manifest.dependencies)?.into_iter()); + local_dependencies.extend(read_dependencies(path, &manifest.dev_dependencies)?.into_iter()); + local_dependencies.extend(read_dependencies(path, &manifest.build_dependencies)?.into_iter()); + Ok(Package::new(handle, path, local_dependencies)) +} + +/// Validates that all of the publishable crates use consistent version numbers +/// across all of their local dependencies. +fn validate_packages(packages: &Vec) -> Result<()> { + let mut versions: BTreeMap = BTreeMap::new(); + let track_version = &mut |handle: &PackageHandle| -> Result<(), Error> { + if let Some(version) = versions.get(&handle.name) { + if *version != handle.version { + Err(Error::MultipleVersions( + (&handle.name).into(), + versions[&handle.name].clone(), + handle.version.clone(), + )) + } else { + Ok(()) + } + } else { + versions.insert(handle.name.clone(), handle.version.clone()); + Ok(()) + } + }; + for package in packages { + track_version(&package.handle)?; + for dependency in &package.local_dependencies { + track_version(dependency)?; + } + } + + Ok(()) +} + +async fn read_packages(fs: Fs, manifest_paths: Vec) -> Result> { + let mut result = Vec::new(); + for path in &manifest_paths { + let contents: Vec = fs.read_file(path).await?; + result.push(read_package(&path, &contents)?); + } + Ok(result) +} + +/// Splits the given packages into a list of batches that can be published in order. +/// All of the packages in a given batch can be safely published in parallel. +fn batch_packages(packages: Vec) -> Result> { + // Sort packages in order of local dependencies + let mut packages = dependency_order(packages)?; + + // Discover batches + let mut batches = Vec::new(); + 'outer: while packages.len() > 1 { + for run in 0..packages.len() { + let next = &packages[run]; + // If the next package depends on any prior package, then we've discovered the end of the batch + for index in 0..run { + let previous = &packages[index]; + if next.locally_depends_on(&previous.handle) { + let remaining = packages.split_off(run); + let batch = packages; + packages = remaining; + batches.push(batch); + continue 'outer; + } + } + } + // If the current run is the length of the package vec, then we have exactly one batch left + break; + } + + // Push the final batch + if !packages.is_empty() { + batches.push(packages); + } + Ok(batches) +} + +#[cfg(test)] +mod tests { + use super::*; + use semver::Version; + use std::path::PathBuf; + + fn version(version: &str) -> Version { + Version::parse(version).unwrap() + } + + #[test] + fn read_package_success() { + let manifest = br#" + [package] + name = "test" + version = "1.2.0-preview" + + [build-dependencies] + build_something = "1.3" + local_build_something = { version = "0.2.0", path = "../local_build_something" } + + [dev-dependencies] + dev_something = "1.1" + local_dev_something = { version = "0.1.0", path = "../local_dev_something" } + + [dependencies] + something = "1.0" + local_something = { version = "1.1.3", path = "../local_something" } + "#; + let path: PathBuf = "test/Cargo.toml".into(); + + let package = read_package(&path, manifest).expect("parse success"); + assert_eq!("test", package.handle.name); + assert_eq!(version("1.2.0-preview"), package.handle.version); + + let mut expected = BTreeSet::new(); + expected.insert(PackageHandle::new( + "local_build_something", + version("0.2.0"), + )); + expected.insert(PackageHandle::new("local_dev_something", version("0.1.0"))); + expected.insert(PackageHandle::new("local_something", version("1.1.3"))); + assert_eq!(expected, package.local_dependencies); + } + + #[test] + fn read_package_version_requirement_invalid() { + let manifest = br#" + [package] + name = "test" + version = "1.2.0-preview" + + [dependencies] + local_something = { version = "1.0", path = "../local_something" } + "#; + let path: PathBuf = "test/Cargo.toml".into(); + + let error = format!( + "{}", + read_package(&path, manifest).err().expect("should fail") + ); + assert!( + error.contains("Invalid crate version"), + "'{}' should contain 'Invalid crate version'", + error + ); + } + + fn package(name: &str, dependencies: &[&str]) -> Package { + Package::new( + PackageHandle::new(name, Version::parse("1.0.0").unwrap()), + format!("{}/Cargo.toml", name), + dependencies + .iter() + .map(|d| PackageHandle::new(*d, Version::parse("1.0.0").unwrap())) + .collect(), + ) + } + + fn fmt_batches(batches: Vec) -> String { + let mut result = String::new(); + for batch in batches { + result.push_str( + &batch + .iter() + .map(|p| p.handle.name.as_str()) + .collect::>() + .join(","), + ); + result.push(';'); + } + result + } + + #[test] + fn test_batch_packages() { + assert_eq!("", fmt_batches(batch_packages(vec![]).unwrap())); + assert_eq!( + "A;", + fmt_batches(batch_packages(vec![package("A", &[])]).unwrap()) + ); + assert_eq!( + "A,B;", + fmt_batches(batch_packages(vec![package("A", &[]), package("B", &[])]).unwrap()) + ); + assert_eq!( + "A,B;C;", + fmt_batches( + batch_packages(vec![ + package("C", &["A", "B"]), + package("B", &[]), + package("A", &[]), + ]) + .unwrap() + ) + ); + assert_eq!( + "A,B;C,F,D;E;", + fmt_batches( + batch_packages(vec![ + package("A", &[]), + package("B", &[]), + package("C", &["A"]), + package("D", &["A", "B"]), + package("F", &["B"]), + package("E", &["C", "D", "F"]), + ]) + .unwrap() + ) + ); + } + + fn pkg_ver(name: &str, version: &str, dependencies: &[(&str, &str)]) -> Package { + Package::new( + PackageHandle::new(name, Version::parse(version).unwrap()), + format!("{}/Cargo.toml", name), + dependencies + .iter() + .map(|p| PackageHandle::new(p.0, Version::parse(p.1).unwrap())) + .collect(), + ) + } + + #[test] + fn test_validate_packages() { + validate_packages(&vec![ + pkg_ver("A", "1.0.0", &[]), + pkg_ver("B", "1.1.0", &[]), + pkg_ver("C", "1.2.0", &[("A", "1.0.0"), ("B", "1.1.0")]), + pkg_ver("D", "1.3.0", &[("A", "1.0.0")]), + pkg_ver("F", "1.4.0", &[("B", "1.1.0")]), + pkg_ver( + "E", + "1.5.0", + &[("C", "1.2.0"), ("D", "1.3.0"), ("F", "1.4.0")], + ), + ]) + .expect("success"); + + let error = validate_packages(&vec![ + pkg_ver("A", "1.1.0", &[]), + pkg_ver("B", "1.1.0", &[]), + pkg_ver("C", "1.2.0", &[("A", "1.1.0"), ("B", "1.1.0")]), + pkg_ver("D", "1.3.0", &[("A", "1.0.0")]), + pkg_ver("F", "1.4.0", &[("B", "1.1.0")]), + pkg_ver( + "E", + "1.5.0", + &[("C", "1.2.0"), ("D", "1.3.0"), ("F", "1.4.0")], + ), + ]) + .err() + .expect("fail"); + assert_eq!( + "crate A has multiple versions: 1.1.0 and 1.0.0", + format!("{}", error) + ); + } +} diff --git a/tools/publisher/src/repo.rs b/tools/publisher/src/repo.rs new file mode 100644 index 000000000000..a04108661fdd --- /dev/null +++ b/tools/publisher/src/repo.rs @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Repository discovery. + +use anyhow::Result; +use std::env; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +/// Git repository containing crates to be published. +#[derive(Debug)] +pub struct Repository { + pub root: PathBuf, + pub crates_root: PathBuf, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("failed to find {0} repository root")] + RepositoryRootNotFound(String), +} + +/// Attempts to find git repository root from current working directory. +pub fn discover_repository(name: &str, crate_path: &str) -> Result { + let mut current_dir = env::current_dir()?.canonicalize()?; + let os_name = OsStr::new(name); + loop { + if is_git_root(¤t_dir) { + if let Some(file_name) = current_dir.file_name() { + if os_name == file_name { + return Ok(Repository { + crates_root: current_dir.join(crate_path), + root: current_dir, + }); + } + } + return Err(Error::RepositoryRootNotFound(name.into()).into()); + } else if !current_dir.pop() { + return Err(Error::RepositoryRootNotFound(name.into()).into()); + } + } +} + +fn is_git_root(path: &Path) -> bool { + let path = path.join(".git"); + path.exists() && path.is_dir() +} diff --git a/tools/publisher/src/sort.rs b/tools/publisher/src/sort.rs new file mode 100644 index 000000000000..26e926cf369b --- /dev/null +++ b/tools/publisher/src/sort.rs @@ -0,0 +1,133 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Logic for topological sorting packages by dependencies. + +use crate::package::{Package, PackageHandle}; +use anyhow::Result; +use std::collections::{BTreeMap, BTreeSet}; + +/// Determines the dependency order of the given packages. +pub fn dependency_order(packages: Vec) -> Result> { + let mut order = Vec::new(); + let mut packages: BTreeMap = packages + .into_iter() + .map(|p| (p.handle.clone(), p)) + .collect(); + let mut visited = BTreeSet::new(); + + let mut to_visit: Vec<&Package> = packages.iter().map(|e| e.1).collect(); + to_visit.sort_by(|a, b| { + (*a).local_dependencies + .len() + .cmp(&(*b).local_dependencies.len()) + }); + + // Depth-first search topological sort + loop { + if let Some(package) = to_visit + .iter() + .filter(|e| !visited.contains(&e.handle)) + .next() + { + dependency_order_visit( + &package.handle, + &packages, + &mut BTreeSet::new(), + &mut visited, + &mut order, + )?; + } else { + break; + } + } + + Ok(order + .into_iter() + .map(&mut |handle| packages.remove(&handle).unwrap()) + .collect()) +} + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("dependency cycle detected")] + DependencyCycle, +} + +fn dependency_order_visit( + package_handle: &PackageHandle, + packages: &BTreeMap, + stack: &mut BTreeSet, + visited: &mut BTreeSet, + result: &mut Vec, +) -> Result<(), Error> { + visited.insert(package_handle.clone()); + stack.insert(package_handle.clone()); + + let local_dependencies = &packages[package_handle].local_dependencies; + for dependency in local_dependencies { + if visited.contains(dependency) && stack.contains(dependency) { + return Err(Error::DependencyCycle); + } + if package_handle != dependency + && packages.contains_key(&dependency) + && !visited.contains(dependency) + { + dependency_order_visit(&dependency, packages, stack, visited, result)?; + } + } + result.push(package_handle.clone()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use semver::Version; + + fn package(name: &str, dependencies: &[&str]) -> Package { + Package::new( + PackageHandle::new(name, Version::parse("1.0.0").unwrap()), + format!("{}/Cargo.toml", name), + dependencies + .iter() + .map(|d| PackageHandle::new(*d, Version::parse("1.0.0").unwrap())) + .collect(), + ) + } + + #[test] + pub fn test_dependency_order() { + let packages = vec![ + package("E", &["B", "C", "A"]), + package("B", &[]), + package("F", &["E", "D"]), + package("C", &["A"]), + package("A", &[]), + package("D", &["C"]), + ]; + + let result = dependency_order(packages).unwrap(); + assert_eq!( + "ABCDEF", + result.iter().fold(String::new(), |mut acc, p| { + acc.push_str(&p.handle.name); + acc + }) + ); + } + + #[test] + pub fn test_dependency_cycles() { + let packages = vec![ + package("A", &["C"]), + package("B", &["A"]), + package("C", &["B"]), + ]; + + let error = dependency_order(packages).err().expect("cycle"); + assert_eq!("dependency cycle detected", format!("{}", error)); + } +} diff --git a/tools/publisher/src/subcommand/fix_manifests.rs b/tools/publisher/src/subcommand/fix_manifests.rs new file mode 100644 index 000000000000..1b20039d564b --- /dev/null +++ b/tools/publisher/src/subcommand/fix_manifests.rs @@ -0,0 +1,185 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Subcommand for fixing manifest dependency version numbers. + +use crate::fs::Fs; +use crate::package::{discover_package_manifests, parse_version}; +use crate::repo::discover_repository; +use crate::{REPO_CRATE_PATH, REPO_NAME}; +use anyhow::{Context, Result}; +use cargo_toml::{Dependency, DepsSet}; +use semver::Version; +use std::collections::BTreeMap; +use std::path::PathBuf; +use tracing::info; + +pub async fn subcommand_fix_manifests() -> Result<()> { + let repo = discover_repository(REPO_NAME, REPO_CRATE_PATH)?; + let manifest_paths = discover_package_manifests(&repo.crates_root).await?; + let mut manifests = read_manifests(Fs::Real, manifest_paths).await?; + let versions = package_versions(&manifests)?; + fix_manifests(Fs::Real, &versions, &mut manifests).await?; + Ok(()) +} + +struct Manifest { + path: PathBuf, + metadata: cargo_toml::Manifest, +} + +async fn read_manifests(fs: Fs, manifest_paths: Vec) -> Result> { + let mut result = Vec::new(); + for path in manifest_paths { + let contents = fs.read_file(&path).await?; + let metadata = cargo_toml::Manifest::from_slice(&contents) + .with_context(|| format!("failed to load package manifest for {:?}", &path))?; + result.push(Manifest { path, metadata }); + } + Ok(result) +} + +fn package_versions(manifests: &Vec) -> Result> { + let mut versions = BTreeMap::new(); + for manifest in manifests { + if let Some(package) = &manifest.metadata.package { + let version = parse_version(&manifest.path, &package.version)?; + versions.insert(package.name.clone(), version); + } + } + Ok(versions) +} + +fn fix_dep_set(versions: &BTreeMap, dependencies: &mut DepsSet) -> Result { + let mut changed = 0; + for (dep_name, dep) in dependencies { + changed += match dep { + Dependency::Simple(_) => 0, + Dependency::Detailed(detailed) => { + if detailed.path.is_some() { + let version = versions.get(dep_name).ok_or_else(|| { + anyhow::Error::msg(format!("version not found for crate {}", dep_name)) + })?; + detailed.version = Some(version.to_string()); + 1 + } else { + 0 + } + } + }; + } + Ok(changed) +} + +fn fix_dep_sets( + versions: &BTreeMap, + metadata: &mut cargo_toml::Manifest, +) -> Result { + let mut changed = fix_dep_set(versions, &mut metadata.dependencies)?; + changed += fix_dep_set(versions, &mut metadata.dev_dependencies)?; + changed += fix_dep_set(versions, &mut metadata.build_dependencies)?; + Ok(changed) +} + +async fn fix_manifests( + fs: Fs, + versions: &BTreeMap, + manifests: &mut Vec, +) -> Result<()> { + for manifest in manifests { + let changed = fix_dep_sets(versions, &mut manifest.metadata)?; + if changed > 0 { + let contents = toml::to_string(&manifest.metadata) + .with_context(|| format!("failed to serialize to toml for {:?}", manifest.path))?; + fs.write_file(&manifest.path, contents.as_bytes()).await?; + info!("Changed {} dependencies in {:?}.", changed, manifest.path); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fix_dep_sets() { + let manifest = br#" + [package] + name = "test" + version = "1.2.0-preview" + + [build-dependencies] + build_something = "1.3" + local_build_something = { path = "../local_build_something" } + + [dev-dependencies] + dev_something = "1.1" + local_dev_something = { path = "../local_dev_something" } + + [dependencies] + something = "1.0" + local_something = { path = "../local_something" } + "#; + let metadata = cargo_toml::Manifest::from_slice(manifest).unwrap(); + let mut manifest = Manifest { + path: "test".into(), + metadata, + }; + let versions = vec![ + ("local_build_something", "0.2.0"), + ("local_dev_something", "0.1.0"), + ("local_something", "1.1.3"), + ] + .into_iter() + .map(|e| (e.0.to_string(), Version::parse(e.1).unwrap())) + .collect(); + + fix_dep_sets(&versions, &mut manifest.metadata).expect("success"); + + let actual_deps = toml::Value::try_from(&manifest.metadata.dependencies).unwrap(); + assert_eq!( + "\ + something = \"1.0\"\n\ + \n\ + [local_something]\n\ + features = []\n\ + optional = false\n\ + path = \"../local_something\"\n\ + version = \"1.1.3\"\n\ + ", + actual_deps.to_string() + ); + + let actual_dev_deps = toml::Value::try_from(&manifest.metadata.dev_dependencies).unwrap(); + assert_eq!( + "\ + dev_something = \"1.1\"\n\ + \n\ + [local_dev_something]\n\ + features = []\n\ + optional = false\n\ + path = \"../local_dev_something\"\n\ + version = \"0.1.0\"\n\ + ", + actual_dev_deps.to_string() + ); + + let actual_build_deps = + toml::Value::try_from(&manifest.metadata.build_dependencies).unwrap(); + assert_eq!( + "\ + build_something = \"1.3\"\n\ + \n\ + [local_build_something]\n\ + features = []\n\ + optional = false\n\ + path = \"../local_build_something\"\n\ + version = \"0.2.0\"\n\ + ", + actual_build_deps.to_string() + ); + } +} diff --git a/tools/publisher/src/subcommand/mod.rs b/tools/publisher/src/subcommand/mod.rs new file mode 100644 index 000000000000..6a214ae5f316 --- /dev/null +++ b/tools/publisher/src/subcommand/mod.rs @@ -0,0 +1,7 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +pub mod fix_manifests; +pub mod publish; diff --git a/tools/publisher/src/subcommand/publish.rs b/tools/publisher/src/subcommand/publish.rs new file mode 100644 index 000000000000..2c7eccfc7903 --- /dev/null +++ b/tools/publisher/src/subcommand/publish.rs @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use crate::cargo; +use crate::fs::Fs; +use crate::package::{discover_package_batches, PackageBatch}; +use crate::repo::discover_repository; +use crate::{REPO_CRATE_PATH, REPO_NAME}; +use anyhow::Result; +use dialoguer::Confirm; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Semaphore; +use tracing::info; + +const BACKOFF: Duration = Duration::from_millis(30); +const MAX_CONCURRENCY: usize = 4; + +pub async fn subcommand_publish() -> Result<()> { + // Make sure cargo exists + cargo::confirm_installed_on_path().await?; + + info!("Discovering crates to publish..."); + let repo = discover_repository(REPO_NAME, REPO_CRATE_PATH)?; + let batches = discover_package_batches(Fs::Real, &repo.crates_root).await?; + info!("Crates discovered."); + + // Don't proceed unless the user confirms the plan + confirm_plan(&batches)?; + + // Use a semaphore to only allow a few concurrent publishes + let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENCY)); + for batch in batches { + let mut tasks = Vec::new(); + for package in batch { + let permit = semaphore.clone().acquire_owned().await.unwrap(); + tasks.push(tokio::spawn(async move { + let task = cargo::publish_task(&package.crate_path); + let plan = task.plan(); + info!("Executing `{}`...", plan); + let output = task.spawn().await?; + if !output.status.success() { + let message = format!( + "Cargo publish failed:\nPlan: {}\nStatus: {}\nStdout: {}\nStderr: {}\n", + plan, + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return Err(anyhow::Error::msg(message)); + } + tokio::time::sleep(BACKOFF).await; + drop(permit); + info!("Success: `{}`", plan); + Ok(()) + })); + } + for task in tasks { + task.await??; + } + } + + Ok(()) +} + +fn confirm_plan(batches: &Vec) -> Result<()> { + let mut full_plan = Vec::new(); + for batch in batches { + for package in batch { + full_plan.push(cargo::publish_task(&package.crate_path).plan()); + } + full_plan.push("wait".into()); + } + + println!("Publish plan:"); + for item in full_plan { + println!(" {}", item); + } + + if Confirm::new() + .with_prompt("Continuing will publish to crates.io. Do you wish to continue?") + .interact()? + { + Ok(()) + } else { + Err(anyhow::Error::msg("aborted")) + } +} From 5222e2e2448a6be3e7dea90906e4a267fc6b2af7 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Thu, 14 Oct 2021 14:43:42 -0700 Subject: [PATCH 2/6] CR feedback --- tools/publisher/Cargo.toml | 1 + tools/publisher/src/main.rs | 4 +-- tools/publisher/src/package.rs | 42 +++++++++++++++++++++-- tools/publisher/src/repo.rs | 3 +- tools/publisher/src/subcommand/publish.rs | 27 ++++++++++----- 5 files changed, 64 insertions(+), 13 deletions(-) diff --git a/tools/publisher/Cargo.toml b/tools/publisher/Cargo.toml index 225941bfd71e..48a9a6beebad 100644 --- a/tools/publisher/Cargo.toml +++ b/tools/publisher/Cargo.toml @@ -12,6 +12,7 @@ anyhow = "1.0" cargo_toml = "0.10.1" clap = "2.33" dialoguer = "0.8" +num_cpus = "1.13" semver = "1.0" thiserror = "1.0" tokio = { version = "1.12", features = ["full"] } diff --git a/tools/publisher/src/main.rs b/tools/publisher/src/main.rs index dfc759890968..89bfe6491d5c 100644 --- a/tools/publisher/src/main.rs +++ b/tools/publisher/src/main.rs @@ -15,8 +15,8 @@ mod repo; mod sort; mod subcommand; -pub const REPO_NAME: &'static str = "aws-sdk-rust"; -pub const REPO_CRATE_PATH: &'static str = "sdk"; +pub const REPO_NAME: &str = "aws-sdk-rust"; +pub const REPO_CRATE_PATH: &str = "sdk"; #[tokio::main] async fn main() -> Result<()> { diff --git a/tools/publisher/src/package.rs b/tools/publisher/src/package.rs index d09b0c7b3ee9..102a1dadaad2 100644 --- a/tools/publisher/src/package.rs +++ b/tools/publisher/src/package.rs @@ -14,6 +14,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::error::Error as StdError; use std::path::{Path, PathBuf}; use tokio::fs; +use tracing::warn; /// Information required to identify a package (crate). #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] @@ -64,13 +65,50 @@ impl Package { /// Batch of packages. pub type PackageBatch = Vec; +/// Stats about the packages. +#[derive(Copy, Clone, Debug, Default)] +pub struct PackageStats { + /// Number of Smithy runtime crates + pub smithy_runtime_crates: usize, + /// Number of AWS runtime crates + pub aws_runtime_crates: usize, + /// Number of AWS service crates + pub aws_sdk_crates: usize, +} + +impl PackageStats { + pub fn total(&self) -> usize { + self.smithy_runtime_crates + self.aws_runtime_crates + self.aws_sdk_crates + } + + fn calculate(packages: &[Package]) -> PackageStats { + let mut stats = PackageStats::default(); + for package in packages { + if package.handle.name.starts_with("smithy-") { + stats.smithy_runtime_crates += 1; + } else if package.handle.name.starts_with("aws-sdk-") { + stats.aws_sdk_crates += 1; + } else if package.handle.name.starts_with("aws-") { + stats.aws_runtime_crates += 1; + } else { + warn!("Unrecognized crate name: {}", package.handle.name); + } + } + stats + } +} + /// Discovers publishable packages in the given directory and returns them as /// batches that can be published in order. -pub async fn discover_package_batches(fs: Fs, path: impl AsRef) -> Result> { +pub async fn discover_package_batches( + fs: Fs, + path: impl AsRef, +) -> Result<(Vec, PackageStats)> { let manifest_paths = discover_package_manifests(path).await?; let packages = read_packages(fs, manifest_paths).await?; + let stats = PackageStats::calculate(&packages); validate_packages(&packages)?; - batch_packages(packages) + Ok((batch_packages(packages)?, stats)) } type BoxError = Box; diff --git a/tools/publisher/src/repo.rs b/tools/publisher/src/repo.rs index a04108661fdd..265ede659f50 100644 --- a/tools/publisher/src/repo.rs +++ b/tools/publisher/src/repo.rs @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0. */ -//! Repository discovery. +//! Local filesystem git repository discovery. This enables the tool to +//! orient itself despite being run anywhere from within the git repo. use anyhow::Result; use std::env; diff --git a/tools/publisher/src/subcommand/publish.rs b/tools/publisher/src/subcommand/publish.rs index 2c7eccfc7903..b30157946339 100644 --- a/tools/publisher/src/subcommand/publish.rs +++ b/tools/publisher/src/subcommand/publish.rs @@ -5,7 +5,7 @@ use crate::cargo; use crate::fs::Fs; -use crate::package::{discover_package_batches, PackageBatch}; +use crate::package::{discover_package_batches, PackageBatch, PackageStats}; use crate::repo::discover_repository; use crate::{REPO_CRATE_PATH, REPO_NAME}; use anyhow::Result; @@ -16,7 +16,6 @@ use tokio::sync::Semaphore; use tracing::info; const BACKOFF: Duration = Duration::from_millis(30); -const MAX_CONCURRENCY: usize = 4; pub async fn subcommand_publish() -> Result<()> { // Make sure cargo exists @@ -24,14 +23,19 @@ pub async fn subcommand_publish() -> Result<()> { info!("Discovering crates to publish..."); let repo = discover_repository(REPO_NAME, REPO_CRATE_PATH)?; - let batches = discover_package_batches(Fs::Real, &repo.crates_root).await?; - info!("Crates discovered."); + let (batches, stats) = discover_package_batches(Fs::Real, &repo.crates_root).await?; + info!("Finished crate discovery."); // Don't proceed unless the user confirms the plan - confirm_plan(&batches)?; + confirm_plan(&batches, stats)?; // Use a semaphore to only allow a few concurrent publishes - let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENCY)); + let max_concurrency = num_cpus::get_physical(); + let semaphore = Arc::new(Semaphore::new(max_concurrency)); + info!( + "Will publish {} crates in parallel where possible.", + max_concurrency + ); for batch in batches { let mut tasks = Vec::new(); for package in batch { @@ -65,7 +69,7 @@ pub async fn subcommand_publish() -> Result<()> { Ok(()) } -fn confirm_plan(batches: &Vec) -> Result<()> { +fn confirm_plan(batches: &Vec, stats: PackageStats) -> Result<()> { let mut full_plan = Vec::new(); for batch in batches { for package in batch { @@ -74,10 +78,17 @@ fn confirm_plan(batches: &Vec) -> Result<()> { full_plan.push("wait".into()); } - println!("Publish plan:"); + info!("Publish plan:"); for item in full_plan { println!(" {}", item); } + info!( + "Will publish {} crates total ({} Smithy runtime, {} AWS runtime, {} AWS SDK).", + stats.total(), + stats.smithy_runtime_crates, + stats.aws_runtime_crates, + stats.aws_sdk_crates + ); if Confirm::new() .with_prompt("Continuing will publish to crates.io. Do you wish to continue?") From c740a1dd4e37a3bfe4f479b94229187c303cfa3f Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Thu, 14 Oct 2021 14:47:22 -0700 Subject: [PATCH 3/6] Fix clippy lints --- tools/publisher/src/cargo.rs | 4 ++-- tools/publisher/src/package.rs | 2 +- tools/publisher/src/sort.rs | 24 +++++++------------ .../publisher/src/subcommand/fix_manifests.rs | 2 +- tools/publisher/src/subcommand/publish.rs | 2 +- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/tools/publisher/src/cargo.rs b/tools/publisher/src/cargo.rs index a21b54dd4300..19af140cf6fc 100644 --- a/tools/publisher/src/cargo.rs +++ b/tools/publisher/src/cargo.rs @@ -59,7 +59,7 @@ impl Cmd { if let Some(working_dir) = &self.working_dir { plan.push_str(&format!("[in {:?}]: ", working_dir)); } - plan.push_str(&self.parts.join(" ").to_string()); + plan.push_str(&self.parts.join(" ")); plan } @@ -76,7 +76,7 @@ impl Cmd { impl From for Command { fn from(cmd: Cmd) -> Self { - assert!(cmd.parts.len() > 0); + assert!(!cmd.parts.is_empty()); let mut command = Command::new(&cmd.parts[0]); for i in 1..cmd.parts.len() { command.arg(&cmd.parts[i]); diff --git a/tools/publisher/src/package.rs b/tools/publisher/src/package.rs index 102a1dadaad2..bd96388778bc 100644 --- a/tools/publisher/src/package.rs +++ b/tools/publisher/src/package.rs @@ -190,7 +190,7 @@ fn read_package(path: &Path, manifest_bytes: &[u8]) -> Result { /// Validates that all of the publishable crates use consistent version numbers /// across all of their local dependencies. -fn validate_packages(packages: &Vec) -> Result<()> { +fn validate_packages(packages: &[Package]) -> Result<()> { let mut versions: BTreeMap = BTreeMap::new(); let track_version = &mut |handle: &PackageHandle| -> Result<(), Error> { if let Some(version) = versions.get(&handle.name) { diff --git a/tools/publisher/src/sort.rs b/tools/publisher/src/sort.rs index 26e926cf369b..ff3f4a63b8eb 100644 --- a/tools/publisher/src/sort.rs +++ b/tools/publisher/src/sort.rs @@ -26,22 +26,14 @@ pub fn dependency_order(packages: Vec) -> Result> { }); // Depth-first search topological sort - loop { - if let Some(package) = to_visit - .iter() - .filter(|e| !visited.contains(&e.handle)) - .next() - { - dependency_order_visit( - &package.handle, - &packages, - &mut BTreeSet::new(), - &mut visited, - &mut order, - )?; - } else { - break; - } + while let Some(package) = to_visit.iter().find(|e| !visited.contains(&e.handle)) { + dependency_order_visit( + &package.handle, + &packages, + &mut BTreeSet::new(), + &mut visited, + &mut order, + )?; } Ok(order diff --git a/tools/publisher/src/subcommand/fix_manifests.rs b/tools/publisher/src/subcommand/fix_manifests.rs index 1b20039d564b..e26b0d1e30ea 100644 --- a/tools/publisher/src/subcommand/fix_manifests.rs +++ b/tools/publisher/src/subcommand/fix_manifests.rs @@ -41,7 +41,7 @@ async fn read_manifests(fs: Fs, manifest_paths: Vec) -> Result) -> Result> { +fn package_versions(manifests: &[Manifest]) -> Result> { let mut versions = BTreeMap::new(); for manifest in manifests { if let Some(package) = &manifest.metadata.package { diff --git a/tools/publisher/src/subcommand/publish.rs b/tools/publisher/src/subcommand/publish.rs index b30157946339..cb4efdbe16f9 100644 --- a/tools/publisher/src/subcommand/publish.rs +++ b/tools/publisher/src/subcommand/publish.rs @@ -69,7 +69,7 @@ pub async fn subcommand_publish() -> Result<()> { Ok(()) } -fn confirm_plan(batches: &Vec, stats: PackageStats) -> Result<()> { +fn confirm_plan(batches: &[PackageBatch], stats: PackageStats) -> Result<()> { let mut full_plan = Vec::new(); for batch in batches { for package in batch { From cc155adefeb818ffc8d51b4d9fef321f4b44ec9c Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Thu, 14 Oct 2021 15:53:03 -0700 Subject: [PATCH 4/6] Add tools to CI --- .github/workflows/ci.yaml | 2 +- .github/workflows/tool-ci.yaml | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tool-ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5ddccac45ce0..b838ab454aa7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,7 @@ on: [ pull_request ] env: - rust_version: 1.52.1 + rust_version: 1.53.0 name: CI diff --git a/.github/workflows/tool-ci.yaml b/.github/workflows/tool-ci.yaml new file mode 100644 index 000000000000..1453ce191f2a --- /dev/null +++ b/.github/workflows/tool-ci.yaml @@ -0,0 +1,39 @@ +on: + pull_request: + paths: 'tools/**' + +env: + rust_version: 1.53.0 + rust_toolchain_components: clippy,rustfmt + +name: Tools CI + +jobs: + test: + runs-on: ubuntu-latest + name: Compile, Test, and Lint the `tools/` path + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + name: Cargo Cache + with: + path: | + ~/.cargo/registry + ~/.cargo/git + tools/publisher/target + key: tools-${{ runner.os }}-cargo-${{ hashFiles('tools/**/Cargo.toml') }} + restore-keys: | + tools-${{ runner.os }}-cargo- + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.rust_version }} + components: ${{ env.rust_toolchain_components }} + default: true + - name: Format Check + run: rustfmt --check --edition 2018 $(find tools -name '*.rs' -print | grep -v /target/) + - name: Cargo Test + run: cargo test + working-directory: tools/publisher + - name: Cargo Clippy + run: cargo clippy -- -D warnings + working-directory: tools/publisher From 97697bb2f821a0c140332f45dc049accd07e74d4 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Thu, 14 Oct 2021 16:20:20 -0700 Subject: [PATCH 5/6] Only run SDK CI when changing the SDK --- .github/workflows/ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b838ab454aa7..b7c6eb3cbb8e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,6 @@ -on: [ pull_request ] +on: + pull_request: + paths: 'sdk/**' env: rust_version: 1.53.0 From 7ef6cbe56d1d4ce2e3b90f307fa3f058199f6104 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Thu, 14 Oct 2021 16:38:06 -0700 Subject: [PATCH 6/6] Revert "Only run SDK CI when changing the SDK" --- .github/workflows/ci.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b7c6eb3cbb8e..b838ab454aa7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,4 @@ -on: - pull_request: - paths: 'sdk/**' +on: [ pull_request ] env: rust_version: 1.53.0