Fix SDK canary by pinning SDK versions to smithy-rs versions (#1145)

* Move canary-runner tool from aws-sdk-rust

* Commonize git shell operations

* Centralize package categorization logic

* Pin smithy-rs revisions to SDK versions for the canary

* Incorporate feedback
This commit is contained in:
John DiSanti 2022-02-02 16:53:02 -08:00 committed by GitHub
parent 4c7515f6a8
commit 11c61f65c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2970 additions and 225 deletions

1917
tools/ci-cdk/canary-runner/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
[package]
name = "canary-runner"
version = "0.1.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
description = "Tool used to run the canary tests in CI"
edition = "2018"
license = "Apache-2.0"
publish = false
[workspace]
[dependencies]
anyhow = "1"
aws-config = "0.3"
aws-sdk-cloudwatch = "0.3"
aws-sdk-lambda = "0.3"
aws-sdk-s3 = "0.3"
base64 = "0.13"
crates_io_api = "0.7"
lazy_static = "1"
semver = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
structopt = "0.3"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
smithy-rs-tool-common = { version = "0.1", path = "../../smithy-rs-tool-common", features = ["async-shell"] }

View File

@ -0,0 +1,11 @@
#!/bin/bash
#
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.
#
# Run by CI to check the canary-lambda
set -e
cd "$(dirname $0)"
cargo test
cargo clippy

View File

@ -0,0 +1,51 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
use anyhow::Result;
use crates_io_api::AsyncClient;
use lazy_static::lazy_static;
use serde::Serialize;
use std::time::Duration;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
pub struct GenerateMatrixOpt {
#[structopt(short, long)]
sdk_versions: u8,
#[structopt(short, long)]
rust_versions: Vec<String>,
}
lazy_static! {
static ref CRATES_IO_CLIENT: AsyncClient = AsyncClient::new(
"AWS_RUST_SDK_PUBLISHER (aws-sdk-rust@amazon.com)",
Duration::from_secs(1)
)
.expect("valid client");
}
#[derive(Debug, Serialize)]
struct Output {
sdk_version: Vec<String>,
rust_version: Vec<String>,
}
pub async fn generate_matrix(opt: GenerateMatrixOpt) -> Result<()> {
let crate_response = CRATES_IO_CLIENT.get_crate("aws-config").await?;
let output = Output {
// The versions from the Crates IO client come back in descending order by version number
sdk_version: crate_response
.versions
.into_iter()
.filter(|v| !v.yanked)
.map(|v| v.num)
.take(opt.sdk_versions as usize)
.collect(),
rust_version: opt.rust_versions,
};
println!("{}", serde_json::to_string(&output)?);
Ok(())
}

View File

@ -0,0 +1,37 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
use structopt::StructOpt;
use tracing_subscriber::{filter::EnvFilter, prelude::*};
mod generate_matrix;
mod run;
#[derive(StructOpt, Debug)]
#[structopt(name = "canary-runner")]
enum Opt {
#[structopt(alias = "generate-matrix")]
GenerateMatrix(generate_matrix::GenerateMatrixOpt),
#[structopt(alias = "run")]
Run(run::RunOpt),
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("warn,canary_runner=info"))
.unwrap();
tracing_subscriber::registry()
.with(filter_layer)
.with(tracing_subscriber::fmt::layer().with_target(false))
.init();
let opt = Opt::from_args();
match opt {
Opt::GenerateMatrix(subopt) => generate_matrix::generate_matrix(subopt).await,
Opt::Run(subopt) => run::run(subopt).await,
}
}

View File

@ -0,0 +1,351 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
// This is the code used by CI to run the canary Lambda.
//
// If running this locally, you'll need to make a clone of awslabs/smithy-rs in
// the aws-sdk-rust project root.
//
// Also consider using the `AWS_PROFILE` and `AWS_REGION` environment variables
// when running this locally.
use anyhow::{bail, Context, Result};
use aws_sdk_cloudwatch as cloudwatch;
use aws_sdk_lambda as lambda;
use aws_sdk_s3 as s3;
use cloudwatch::model::StandardUnit;
use s3::ByteStream;
use semver::Version;
use smithy_rs_tool_common::git;
use smithy_rs_tool_common::shell::ShellOperation;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use std::{env, path::Path};
use structopt::StructOpt;
use tokio::process::Command;
use tracing::{error, info};
lazy_static::lazy_static! {
// Occasionally, a breaking change introduced in smithy-rs will cause the canary to fail
// for older versions of the SDK since the canary is in the smithy-rs repository and will
// get fixed for that breaking change. When this happens, the older SDK versions can be
// pinned to a commit hash in the smithy-rs repository to get old canary code that still
// compiles against that version of the SDK.
static ref PINNED_SMITHY_RS_VERSIONS: Vec<(Version, &'static str)> = {
let mut pinned = vec![
// Versions <= 0.6.0 no longer compile against the canary after this commit in smithy-rs
// due to the breaking change in https://github.com/awslabs/smithy-rs/pull/1085
(Version::parse("0.6.0").unwrap(), "d48c234796a16d518ca9e1dda5c7a1da4904318c"),
];
pinned.sort();
pinned
};
}
#[derive(StructOpt, Debug)]
pub struct RunOpt {
#[structopt(long, about = "Version of the SDK to compile the canary against")]
sdk_version: String,
#[structopt(
long,
about = "The name of the S3 bucket to upload the canary binary bundle to"
)]
lambda_code_s3_bucket_name: String,
#[structopt(
long,
about = "The name of the S3 bucket for the canary Lambda to interact with"
)]
lambda_test_s3_bucket_name: String,
#[structopt(long, about = "The ARN of the role that the Lambda will execute as")]
lambda_execution_role_arn: String,
}
pub async fn run(opt: RunOpt) -> Result<()> {
let start_time = SystemTime::now();
let config = aws_config::load_from_env().await;
let result = run_canary(opt, &config).await;
let mut metrics = vec![
(
"canary-success",
if result.is_ok() { 1.0 } else { 0.0 },
StandardUnit::Count,
),
(
"canary-failure",
if result.is_ok() { 0.0 } else { 1.0 },
StandardUnit::Count,
),
(
"canary-total-time",
start_time.elapsed().expect("time in range").as_secs_f64(),
StandardUnit::Seconds,
),
];
if let Ok(invoke_time) = result {
metrics.push((
"canary-invoke-time",
invoke_time.as_secs_f64(),
StandardUnit::Seconds,
));
}
let cloudwatch_client = cloudwatch::Client::new(&config);
let mut request_builder = cloudwatch_client
.put_metric_data()
.namespace("aws-sdk-rust-canary");
for metric in metrics {
request_builder = request_builder.metric_data(
cloudwatch::model::MetricDatum::builder()
.metric_name(metric.0)
.value(metric.1)
.timestamp(SystemTime::now().into())
.unit(metric.2)
.build(),
);
}
info!("Emitting metrics...");
request_builder
.send()
.await
.context("failed to emit metrics")?;
result.map(|_| ())
}
async fn run_canary(opt: RunOpt, config: &aws_config::Config) -> Result<Duration> {
let repo_root = git_root().await?;
env::set_current_dir(repo_root.join("smithy-rs/tools/ci-cdk/canary-lambda"))
.context("failed to change working directory")?;
use_correct_revision(&opt).await?;
info!("Generating canary Cargo.toml...");
generate_cargo_toml(&opt.sdk_version).await?;
info!("Building the canary...");
let bundle_path = build_bundle(&opt.sdk_version).await?;
let bundle_file_name = bundle_path.file_name().unwrap().to_str().unwrap();
let bundle_name = bundle_path.file_stem().unwrap().to_str().unwrap();
let s3_client = s3::Client::new(config);
let lambda_client = lambda::Client::new(config);
info!("Uploading Lambda code bundle to S3...");
upload_bundle(
s3_client,
&opt.lambda_code_s3_bucket_name,
bundle_file_name,
&bundle_path,
)
.await?;
info!(
"Creating the canary Lambda function named {}...",
bundle_name
);
create_lambda_fn(
lambda_client.clone(),
bundle_name,
bundle_file_name,
&opt.lambda_execution_role_arn,
&opt.lambda_code_s3_bucket_name,
&opt.lambda_test_s3_bucket_name,
)
.await?;
info!("Invoking the canary Lambda...");
let invoke_start_time = SystemTime::now();
let invoke_result = invoke_lambda(lambda_client.clone(), bundle_name).await;
let invoke_time = invoke_start_time.elapsed().expect("time in range");
info!("Deleting the canary Lambda...");
delete_lambda_fn(lambda_client, bundle_name).await?;
invoke_result.map(|_| invoke_time)
}
async fn use_correct_revision(opt: &RunOpt) -> Result<()> {
let sdk_version = Version::parse(&opt.sdk_version).expect("valid version");
if let Some((version, commit_hash)) = PINNED_SMITHY_RS_VERSIONS
.iter()
.find(|(v, _)| v >= &sdk_version)
{
info!(
"SDK version {} requires smithy-rs@{} to successfully compile the canary",
version, commit_hash
);
let smithy_rs_root = git::find_git_repository_root("smithy-rs", ".")?;
git::CheckoutRevision::new(smithy_rs_root, *commit_hash)
.spawn()
.await?;
}
Ok(())
}
async fn generate_cargo_toml(sdk_version: &str) -> Result<()> {
let status = Command::new("./write-cargo-toml.py")
.arg("--sdk-version")
.arg(sdk_version)
.status()
.await?;
if !status.success() {
bail!("Failed to generate canary Cargo.toml");
}
Ok(())
}
/// Returns the path to the compiled bundle zip file
async fn build_bundle(sdk_version: &str) -> Result<PathBuf> {
let output = Command::new("./build-bundle.sh")
.arg(sdk_version)
.stderr(std::process::Stdio::inherit())
.output()
.await?;
if !output.status.success() {
error!(
"{}",
std::str::from_utf8(&output.stderr).expect("valid utf-8")
);
bail!("Failed to build the canary bundle");
} else {
Ok(PathBuf::from(String::from_utf8(output.stdout)?.trim()))
}
}
async fn upload_bundle(
s3_client: s3::Client,
s3_bucket: &str,
file_name: &str,
bundle_path: &Path,
) -> Result<()> {
s3_client
.put_object()
.bucket(s3_bucket)
.key(file_name)
.body(
ByteStream::from_path(bundle_path)
.await
.context("failed to load bundle file")?,
)
.send()
.await
.context("failed to upload bundle to S3")?;
Ok(())
}
async fn create_lambda_fn(
lambda_client: lambda::Client,
bundle_name: &str,
bundle_file_name: &str,
execution_role: &str,
code_s3_bucket: &str,
test_s3_bucket: &str,
) -> Result<()> {
use lambda::model::*;
lambda_client
.create_function()
.function_name(bundle_name)
.runtime(Runtime::Providedal2)
.role(execution_role)
.handler("aws-sdk-rust-lambda-canary")
.code(
FunctionCode::builder()
.s3_bucket(code_s3_bucket)
.s3_key(bundle_file_name)
.build(),
)
.publish(true)
.environment(
Environment::builder()
.variables("RUST_BACKTRACE", "1")
.variables("CANARY_S3_BUCKET_NAME", test_s3_bucket)
.variables(
"CANARY_EXPECTED_TRANSCRIBE_RESULT",
"Good day to you transcribe. This is Polly talking to you from the Rust ST K.",
)
.build(),
)
.timeout(60)
.send()
.await
.context("failed to create canary Lambda function")?;
let mut attempts = 0;
let mut state = State::Pending;
while !matches!(state, State::Active) && attempts < 20 {
info!("Waiting 1 second for Lambda to become active...");
tokio::time::sleep(Duration::from_secs(1)).await;
let configuration = lambda_client
.get_function_configuration()
.function_name(bundle_name)
.send()
.await
.context("failed to get Lambda function status")?;
state = configuration.state.unwrap();
attempts += 1;
}
if !matches!(state, State::Active) {
bail!("Timed out waiting for canary Lambda to become active");
}
Ok(())
}
async fn invoke_lambda(lambda_client: lambda::Client, bundle_name: &str) -> Result<()> {
use lambda::model::*;
use lambda::Blob;
let response = lambda_client
.invoke()
.function_name(bundle_name)
.invocation_type(InvocationType::RequestResponse)
.log_type(LogType::Tail)
.payload(Blob::new(&b"{}"[..]))
.send()
.await
.context("failed to invoke the canary Lambda")?;
if let Some(log_result) = response.log_result {
info!(
"Last 4 KB of canary logs:\n----\n{}\n----\n",
std::str::from_utf8(&base64::decode(&log_result)?)?
);
}
if response.status_code != 200 {
bail!(
"Canary failed: {}",
response
.function_error
.as_deref()
.unwrap_or("<no error given>")
);
}
Ok(())
}
async fn delete_lambda_fn(lambda_client: lambda::Client, bundle_name: &str) -> Result<()> {
lambda_client
.delete_function()
.function_name(bundle_name)
.send()
.await
.context("failed to delete Lambda")?;
Ok(())
}
async fn git_root() -> Result<PathBuf> {
let output = Command::new("git")
.arg("rev-parse")
.arg("--show-toplevel")
.output()
.await
.context("couldn't find repository root")?;
Ok(PathBuf::from(String::from_utf8(output.stdout)?.trim()))
}

View File

@ -952,6 +952,7 @@ dependencies = [
"regex",
"semver",
"serde_json",
"smithy-rs-tool-common",
"thiserror",
"tokio",
"toml",
@ -1226,6 +1227,15 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "smithy-rs-tool-common"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"tokio",
]
[[package]]
name = "socket2"
version = "0.4.2"

View File

@ -23,6 +23,7 @@ num_cpus = "1.13"
regex = "1.5.4"
semver = "1.0"
serde_json = "1"
smithy-rs-tool-common = { version = "0.1", path = "../smithy-rs-tool-common", features = ["async-shell"] }
thiserror = "1.0"
tokio = { version = "1.12", features = ["full"] }
toml = { version = "0.5.8", features = ["preserve_order"] }

View File

@ -15,8 +15,8 @@ pub use get_owners::GetOwners;
pub use publish::Publish;
pub use yank::Yank;
use crate::shell::handle_failure;
use anyhow::{Context, Result};
use smithy_rs_tool_common::shell::handle_failure;
use std::process::Command;
/// Confirms that cargo exists on the path.

View File

@ -3,39 +3,37 @@
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::shell::{handle_failure, ShellOperation};
use anyhow::Result;
use async_trait::async_trait;
use smithy_rs_tool_common::shell::{handle_failure, ShellOperation};
use std::process::Command;
pub struct AddOwner<'a> {
pub struct AddOwner {
program: &'static str,
package_name: &'a str,
owner: &'a str,
package_name: String,
owner: String,
}
impl<'a> AddOwner<'a> {
pub fn new(package_name: &'a str, owner: &'a str) -> AddOwner<'a> {
impl AddOwner {
pub fn new(package_name: impl Into<String>, owner: impl Into<String>) -> AddOwner {
AddOwner {
program: "cargo",
package_name,
owner,
package_name: package_name.into(),
owner: owner.into(),
}
}
}
#[async_trait]
impl<'a> ShellOperation for AddOwner<'a> {
impl ShellOperation for AddOwner {
type Output = ();
async fn spawn(&self) -> Result<()> {
fn run(&self) -> Result<()> {
let mut command = Command::new(self.program);
command
.arg("owner")
.arg("--add")
.arg(self.owner)
.arg(self.package_name);
let output = tokio::task::spawn_blocking(move || command.output()).await??;
.arg(&self.owner)
.arg(&self.package_name);
let output = command.output()?;
handle_failure("add owner", &output)?;
Ok(())
}
@ -44,13 +42,14 @@ impl<'a> ShellOperation for AddOwner<'a> {
#[cfg(all(test, not(target_os = "windows")))]
mod tests {
use super::*;
use smithy_rs_tool_common::shell::ShellOperation;
#[tokio::test]
async fn add_owner_success() {
AddOwner {
program: "./fake_cargo/cargo_success",
package_name: "aws-sdk-s3",
owner: "github:awslabs:rust-sdk-owners",
package_name: "aws-sdk-s3".into(),
owner: "github:awslabs:rust-sdk-owners".into(),
}
.spawn()
.await
@ -61,8 +60,8 @@ mod tests {
async fn get_owners_failed() {
let result = AddOwner {
program: "./fake_cargo/cargo_fails",
package_name: "aws-sdk-s3",
owner: "github:awslabs:rust-sdk-owners",
package_name: "aws-sdk-s3".into(),
owner: "github:awslabs:rust-sdk-owners".into(),
}
.spawn()
.await;

View File

@ -3,34 +3,32 @@
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::shell::{handle_failure, output_text, ShellOperation};
use anyhow::Result;
use async_trait::async_trait;
use regex::Regex;
use smithy_rs_tool_common::shell::{handle_failure, output_text, ShellOperation};
use std::process::Command;
pub struct GetOwners<'a> {
pub struct GetOwners {
program: &'static str,
package_name: &'a str,
package_name: String,
}
impl<'a> GetOwners<'a> {
pub fn new(package_name: &'a str) -> GetOwners<'a> {
impl GetOwners {
pub fn new(package_name: impl Into<String>) -> GetOwners {
GetOwners {
program: "cargo",
package_name,
package_name: package_name.into(),
}
}
}
#[async_trait]
impl<'a> ShellOperation for GetOwners<'a> {
impl ShellOperation for GetOwners {
type Output = Vec<String>;
async fn spawn(&self) -> Result<Vec<String>> {
fn run(&self) -> Result<Vec<String>> {
let mut command = Command::new(self.program);
command.arg("owner").arg("--list").arg(self.package_name);
let output = tokio::task::spawn_blocking(move || command.output()).await??;
command.arg("owner").arg("--list").arg(&self.package_name);
let output = command.output()?;
handle_failure("get crate owners", &output)?;
let mut result = Vec::new();
@ -59,7 +57,7 @@ mod tests {
async fn get_owners_success() {
let owners = GetOwners {
program: "./fake_cargo/cargo_owner_list",
package_name: "aws-sdk-s3",
package_name: "aws-sdk-s3".into(),
}
.spawn()
.await
@ -77,7 +75,7 @@ mod tests {
async fn get_owners_failed() {
let result = GetOwners {
program: "./fake_cargo/cargo_fails",
package_name: "aws-sdk-s3",
package_name: "aws-sdk-s3".into(),
}
.spawn()
.await;

View File

@ -4,42 +4,40 @@
*/
use crate::package::PackageHandle;
use crate::shell::{capture_error, output_text, ShellOperation};
use anyhow::Result;
use async_trait::async_trait;
use std::path::Path;
use smithy_rs_tool_common::shell::{capture_error, output_text, ShellOperation};
use std::path::PathBuf;
use std::process::Command;
use tracing::info;
pub struct Publish<'a> {
pub struct Publish {
program: &'static str,
package_handle: &'a PackageHandle,
package_path: &'a Path,
package_handle: PackageHandle,
package_path: PathBuf,
}
impl<'a> Publish<'a> {
pub fn new(package_handle: &'a PackageHandle, package_path: &'a Path) -> Publish<'a> {
impl Publish {
pub fn new(package_handle: PackageHandle, package_path: impl Into<PathBuf>) -> Publish {
Publish {
program: "cargo",
package_handle,
package_path,
package_path: package_path.into(),
}
}
}
#[async_trait]
impl<'a> ShellOperation for Publish<'a> {
impl ShellOperation for Publish {
type Output = ();
async fn spawn(&self) -> Result<()> {
fn run(&self) -> Result<()> {
let mut command = Command::new(self.program);
command
.current_dir(self.package_path)
.current_dir(&self.package_path)
.env("CARGO_INCREMENTAL", "0") // Disable incremental compilation to reduce disk space used
.arg("publish")
.arg("--jobs")
.arg("1");
let output = tokio::task::spawn_blocking(move || command.output()).await??;
let output = command.output()?;
if !output.status.success() {
let (stdout, stderr) = output_text(&output);
let already_uploaded_msg = format!(
@ -69,11 +67,11 @@ mod tests {
async fn publish_succeeds() {
Publish {
program: "./fake_cargo/cargo_success",
package_handle: &PackageHandle::new(
package_handle: PackageHandle::new(
"aws-sdk-dynamodb",
Version::parse("0.0.22-alpha").unwrap(),
),
package_path: &env::current_dir().unwrap(),
package_path: env::current_dir().unwrap().into(),
}
.spawn()
.await
@ -84,11 +82,11 @@ mod tests {
async fn publish_fails() {
let result = Publish {
program: "./fake_cargo/cargo_fails",
package_handle: &PackageHandle::new(
package_handle: PackageHandle::new(
"something",
Version::parse("0.0.22-alpha").unwrap(),
),
package_path: &env::current_dir().unwrap(),
package_path: env::current_dir().unwrap().into(),
}
.spawn()
.await;
@ -106,11 +104,11 @@ mod tests {
async fn publish_fails_already_uploaded() {
Publish {
program: "./fake_cargo/cargo_publish_already_published",
package_handle: &PackageHandle::new(
package_handle: PackageHandle::new(
"aws-sdk-dynamodb",
Version::parse("0.0.22-alpha").unwrap(),
),
package_path: &env::current_dir().unwrap(),
package_path: env::current_dir().unwrap().into(),
}
.spawn()
.await

View File

@ -4,43 +4,41 @@
*/
use crate::package::PackageHandle;
use crate::shell::{capture_error, output_text, ShellOperation};
use anyhow::Result;
use async_trait::async_trait;
use std::path::Path;
use smithy_rs_tool_common::shell::{capture_error, output_text, ShellOperation};
use std::path::PathBuf;
use std::process::Command;
use tracing::info;
/// Yanks a package version from crates.io
pub struct Yank<'a> {
pub struct Yank {
program: &'static str,
package_handle: &'a PackageHandle,
package_path: &'a Path,
package_handle: PackageHandle,
package_path: PathBuf,
}
impl<'a> Yank<'a> {
pub fn new(package_handle: &'a PackageHandle, package_path: &'a Path) -> Yank<'a> {
impl Yank {
pub fn new(package_handle: PackageHandle, package_path: impl Into<PathBuf>) -> Yank {
Yank {
program: "cargo",
package_handle,
package_path,
package_path: package_path.into(),
}
}
}
#[async_trait]
impl<'a> ShellOperation for Yank<'a> {
impl ShellOperation for Yank {
type Output = ();
async fn spawn(&self) -> Result<()> {
fn run(&self) -> Result<()> {
let mut command = Command::new(self.program);
command
.current_dir(self.package_path)
.current_dir(&self.package_path)
.arg("yank")
.arg("--vers")
.arg(format!("{}", self.package_handle.version))
.arg(&self.package_handle.name);
let output = tokio::task::spawn_blocking(move || command.output()).await??;
let output = command.output()?;
if !output.status.success() {
let (_, stderr) = output_text(&output);
let no_such_version = format!(
@ -70,11 +68,11 @@ mod tests {
async fn yank_succeeds() {
Yank {
program: "./fake_cargo/cargo_success",
package_handle: &PackageHandle::new(
package_handle: PackageHandle::new(
"aws-sdk-dynamodb",
Version::parse("0.0.22-alpha").unwrap(),
),
package_path: &env::current_dir().unwrap(),
package_path: env::current_dir().unwrap().into(),
}
.spawn()
.await
@ -85,11 +83,11 @@ mod tests {
async fn yank_fails() {
let result = Yank {
program: "./fake_cargo/cargo_fails",
package_handle: &PackageHandle::new(
package_handle: PackageHandle::new(
"something",
Version::parse("0.0.22-alpha").unwrap(),
),
package_path: &env::current_dir().unwrap(),
package_path: env::current_dir().unwrap().into(),
}
.spawn()
.await;
@ -107,8 +105,8 @@ mod tests {
async fn yank_no_such_version() {
Yank {
program: "./fake_cargo/cargo_yank_not_found",
package_handle: &PackageHandle::new("aws-sigv4", Version::parse("0.0.0").unwrap()),
package_path: &env::current_dir().unwrap(),
package_handle: PackageHandle::new("aws-sigv4", Version::parse("0.0.0").unwrap()),
package_path: env::current_dir().unwrap().into(),
}
.spawn()
.await

View File

@ -14,10 +14,8 @@ use subcommand::hydrate_readme::subcommand_hydrate_readme;
mod cargo;
mod fs;
mod git;
mod package;
mod repo;
mod shell;
mod sort;
mod subcommand;

View File

@ -10,6 +10,7 @@ use crate::sort::dependency_order;
use anyhow::{Context, Result};
use cargo_toml::{Dependency, DepsSet, Manifest};
use semver::Version;
use smithy_rs_tool_common::package::PackageCategory;
use std::collections::{BTreeMap, BTreeSet};
use std::error::Error as StdError;
use std::fmt;
@ -17,34 +18,6 @@ use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::warn;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum PackageCategory {
SmithyRuntime,
AwsRuntime,
AwsSdk,
Unknown,
}
impl PackageCategory {
/// Returns true if the category is `AwsRuntime` or `AwsSdk`
pub fn is_sdk(&self) -> bool {
matches!(self, PackageCategory::AwsRuntime | PackageCategory::AwsSdk)
}
/// Categorizes a package based on its name
pub fn from_package_name(name: &str) -> PackageCategory {
if name.starts_with("aws-smithy-") {
PackageCategory::SmithyRuntime
} else if name.starts_with("aws-sdk-") {
PackageCategory::AwsSdk
} else if name.starts_with("aws-") {
PackageCategory::AwsRuntime
} else {
PackageCategory::Unknown
}
}
}
/// Information required to identify a package (crate).
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct PackageHandle {

View File

@ -6,11 +6,10 @@
//! Local filesystem git repository discovery. This enables the tool to
//! orient itself despite being run anywhere from within the git repo.
use crate::git;
use crate::shell::ShellOperation;
use crate::{SDK_REPO_CRATE_PATH, SDK_REPO_NAME};
use anyhow::Result;
use std::ffi::OsStr;
use smithy_rs_tool_common::git;
use smithy_rs_tool_common::shell::ShellOperation;
use std::path::{Path, PathBuf};
/// Git repository containing crates to be published.
@ -20,51 +19,24 @@ pub struct Repository {
}
impl Repository {
pub fn new(repo_name: &str, path: impl Into<PathBuf>) -> Result<Repository> {
let root = git::find_git_repository_root(repo_name, path.into())?;
Ok(Repository { root })
}
/// Returns the current tag of this repository
pub async fn current_tag(&self) -> Result<String> {
git::GetCurrentTag::new(&self.root).spawn().await
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to find {0} repository root")]
RepositoryRootNotFound(String),
}
/// Given a `location`, this function looks for the `aws-sdk-rust` git repository. If found,
/// it resolves the `sdk/` directory. Otherwise, it returns the original `location`.
pub async fn resolve_publish_location(location: &Path) -> PathBuf {
match find_git_repository_root(SDK_REPO_NAME, location).await {
pub fn resolve_publish_location(location: &Path) -> PathBuf {
match Repository::new(SDK_REPO_NAME, location) {
// If the given path was the `aws-sdk-rust` repo root, then resolve the `sdk/` directory to publish from
Ok(sdk_repo) => sdk_repo.root.join(SDK_REPO_CRATE_PATH),
// Otherwise, publish from the given path (likely the smithy-rs runtime bundle)
Err(_) => location.into(),
}
}
/// Attempts to find git repository root from the given location.
pub async fn find_git_repository_root(
repo_name: &str,
location: impl Into<PathBuf>,
) -> Result<Repository> {
let mut current_dir = location.into();
let os_name = OsStr::new(repo_name);
loop {
if is_git_root(&current_dir) {
if let Some(file_name) = current_dir.file_name() {
if os_name == file_name {
return Ok(Repository { root: current_dir });
}
}
return Err(Error::RepositoryRootNotFound(repo_name.into()).into());
} else if !current_dir.pop() {
return Err(Error::RepositoryRootNotFound(repo_name.into()).into());
}
}
}
fn is_git_root(path: &Path) -> bool {
let path = path.join(".git");
path.exists() && path.is_dir()
}

View File

@ -14,8 +14,8 @@ use crate::package::{discover_package_manifests, parse_version};
use crate::SDK_REPO_NAME;
use anyhow::{bail, Context, Result};
use semver::Version;
use smithy_rs_tool_common::github_actions::running_in_github_actions;
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use toml::value::Table;
@ -159,7 +159,7 @@ fn conditionally_disallow_publish(
manifest_path: &Path,
metadata: &mut toml::Value,
) -> Result<bool> {
let is_github_actions = env::var("GITHUB_ACTIONS").unwrap_or_default() == "true";
let is_github_actions = running_in_github_actions();
let is_example = is_example_manifest(manifest_path);
// Safe-guard to prevent accidental publish to crates.io. Add some friction

View File

@ -4,9 +4,10 @@
*/
use crate::fs::Fs;
use crate::package::{discover_and_validate_package_batches, PackageCategory};
use crate::package::discover_and_validate_package_batches;
use anyhow::{anyhow, bail, Result};
use semver::Version;
use smithy_rs_tool_common::package::PackageCategory;
use std::collections::BTreeMap;
use std::path::Path;
use tracing::info;

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::repo::find_git_repository_root;
use crate::repo::Repository;
use crate::SMITHYRS_REPO_NAME;
use anyhow::{Context, Result};
use handlebars::Handlebars;
@ -18,7 +18,7 @@ pub async fn subcommand_hydrate_readme(
output: &Path,
) -> Result<()> {
let cwd = std::env::current_dir()?;
let repository = find_git_repository_root(SMITHYRS_REPO_NAME, &cwd).await?;
let repository = Repository::new(SMITHYRS_REPO_NAME, &cwd)?;
let template_path = repository.root.join("aws/SDK_README.md.hb");
let template_contents = fs::read(&template_path)
.with_context(|| format!("Failed to read README template file at {:?}", template_path))?;

View File

@ -7,14 +7,14 @@ use crate::fs::Fs;
use crate::package::{
discover_and_validate_package_batches, Package, PackageBatch, PackageHandle, PackageStats,
};
use crate::repo::{find_git_repository_root, resolve_publish_location};
use crate::shell::ShellOperation;
use crate::repo::{resolve_publish_location, Repository};
use crate::CRATE_OWNER;
use crate::{cargo, SDK_REPO_NAME};
use anyhow::{bail, Result};
use crates_io_api::{AsyncClient, Error};
use dialoguer::Confirm;
use lazy_static::lazy_static;
use smithy_rs_tool_common::shell::ShellOperation;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
@ -33,7 +33,7 @@ pub async fn subcommand_publish(location: &Path) -> Result<()> {
// Make sure cargo exists
cargo::confirm_installed_on_path()?;
let location = resolve_publish_location(location).await;
let location = resolve_publish_location(location);
info!("Discovering crates to publish...");
let (batches, stats) = discover_and_validate_package_batches(Fs::Real, &location).await?;
@ -60,7 +60,7 @@ pub async fn subcommand_publish(location: &Path) -> Result<()> {
// Only publish if it hasn't been published yet.
if !is_published(&package.handle).await? {
info!("Publishing `{}`...", package.handle);
cargo::Publish::new(&package.handle, &package.crate_path)
cargo::Publish::new(package.handle.clone(), &package.crate_path)
.spawn()
.await?;
// Sometimes it takes a little bit of time for the new package version
@ -94,7 +94,7 @@ async fn confirm_correct_tag(batches: &[Vec<Package>], location: &Path) -> Resul
.next();
if let Some(aws_config_version) = aws_config_version {
let expected_tag = format!("v{}", aws_config_version);
let repository = find_git_repository_root(SDK_REPO_NAME, location).await?;
let repository = Repository::new(SDK_REPO_NAME, location)?;
let current_tag = repository.current_tag().await?;
if expected_tag != current_tag {
bail!(

View File

@ -5,14 +5,13 @@
use crate::cargo;
use crate::fs::Fs;
use crate::package::{
discover_and_validate_package_batches, Package, PackageCategory, PackageHandle, Publish,
};
use crate::package::{discover_and_validate_package_batches, Package, PackageHandle, Publish};
use crate::repo::resolve_publish_location;
use crate::shell::ShellOperation;
use anyhow::{bail, Result};
use dialoguer::Confirm;
use semver::Version;
use smithy_rs_tool_common::package::PackageCategory;
use smithy_rs_tool_common::shell::ShellOperation;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Semaphore;
@ -40,7 +39,7 @@ pub async fn subcommand_yank_category(
// Make sure cargo exists
cargo::confirm_installed_on_path()?;
let location = resolve_publish_location(location).await;
let location = resolve_publish_location(location);
info!("Discovering crates to yank...");
let (batches, _) = discover_and_validate_package_batches(Fs::Real, &location).await?;
@ -81,7 +80,7 @@ pub async fn subcommand_yank_category(
let permit = semaphore.clone().acquire_owned().await.unwrap();
tasks.push(tokio::spawn(async move {
info!("Yanking `{}`...", package.handle);
let result = cargo::Yank::new(&package.handle, &package.crate_path)
let result = cargo::Yank::new(package.handle.clone(), &package.crate_path)
.spawn()
.await;
drop(permit);

View File

@ -17,6 +17,17 @@ version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3"
[[package]]
name = "async-trait"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -228,6 +239,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"pretty_assertions",
"smithy-rs-tool-common",
"structopt",
"tempfile",
"toml",
@ -239,6 +251,14 @@ version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
[[package]]
name = "smithy-rs-tool-common"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
]
[[package]]
name = "strsim"
version = "0.8.0"

View File

@ -12,6 +12,7 @@ publish = false
anyhow = "1.0"
structopt = "0.3"
toml = { version = "0.5.8", features = ["preserve_order"] }
smithy-rs-tool-common = { version = "0.1", path = "../smithy-rs-tool-common" }
[dev-dependencies]
pretty_assertions = "1"

View File

@ -4,6 +4,7 @@
*/
use anyhow::bail;
use smithy_rs_tool_common::package::{PackageCategory, SDK_PREFIX};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
@ -11,18 +12,6 @@ use std::time::Instant;
use structopt::StructOpt;
use toml::value::{Table, Value};
const AWS_CONFIG: &str = "aws-config";
const AWS_RUNTIME_CRATES: &[&str] = &[
"aws-endpoint",
"aws-http",
"aws-hyper",
"aws-sig-auth",
"aws-sigv4",
"aws-types",
];
const SDK_PREFIX: &str = "aws-sdk-";
const SMITHY_PREFIX: &str = "aws-smithy-";
#[derive(StructOpt, Debug)]
#[structopt(
name = "sdk-versioner",
@ -105,7 +94,8 @@ fn update_manifest(manifest_path: &Path, opt: &Opt) -> anyhow::Result<()> {
fn update_dependencies(dependencies: &mut Table, opt: &Opt) -> anyhow::Result<bool> {
let mut changed = false;
for (key, value) in dependencies.iter_mut() {
if is_sdk_or_runtime_crate(key) {
let category = PackageCategory::from_package_name(key);
if !matches!(category, PackageCategory::Unknown) {
if !value.is_table() {
*value = Value::Table(Table::new());
}
@ -116,20 +106,11 @@ fn update_dependencies(dependencies: &mut Table, opt: &Opt) -> anyhow::Result<bo
Ok(changed)
}
fn is_sdk_crate(name: &str) -> bool {
is_service_crate(name) || name == AWS_CONFIG || AWS_RUNTIME_CRATES.iter().any(|&k| k == name)
}
fn is_sdk_or_runtime_crate(name: &str) -> bool {
is_sdk_crate(name) || name.starts_with(SMITHY_PREFIX)
}
fn is_service_crate(name: &str) -> bool {
name.starts_with(SDK_PREFIX)
}
fn crate_path_name(name: &str) -> &str {
if is_service_crate(name) {
if matches!(
PackageCategory::from_package_name(name),
PackageCategory::AwsSdk
) {
&name[SDK_PREFIX.len()..]
} else {
name
@ -137,7 +118,10 @@ fn crate_path_name(name: &str) -> &str {
}
fn update_dependency_value(crate_name: &str, value: &mut Table, opt: &Opt) {
let is_sdk_crate = is_sdk_crate(crate_name);
let is_sdk_crate = matches!(
PackageCategory::from_package_name(crate_name),
PackageCategory::AwsSdk | PackageCategory::AwsRuntime,
);
// Remove keys that will be replaced
value.remove("version");

View File

@ -17,6 +17,17 @@ version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]]
name = "async-trait"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.0.1"
@ -361,10 +372,19 @@ dependencies = [
"git2",
"gitignore",
"pretty_assertions",
"smithy-rs-tool-common",
"structopt",
"tempdir",
]
[[package]]
name = "smithy-rs-tool-common"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
]
[[package]]
name = "structopt"
version = "0.3.25"

View File

@ -14,6 +14,7 @@ authors = ["Zelda Hessler <zhessler@amazon.com>"]
anyhow = "1"
git2 = "0.13"
gitignore = "1"
smithy-rs-tool-common = { version = "0.1", path = "../smithy-rs-tool-common" }
structopt = { version = "0.3", default-features = false }
[dev-dependencies]

View File

@ -6,8 +6,10 @@
mod fs;
use crate::fs::{delete_all_generated_files_and_folders, find_handwritten_files_and_folders};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{bail, Context, Result};
use git2::{Commit, Oid, Repository, ResetType};
use smithy_rs_tool_common::git::GetLastCommit;
use smithy_rs_tool_common::shell::ShellOperation;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
@ -399,7 +401,7 @@ fn create_mirror_commit(aws_sdk_path: &Path, based_on_commit: &Commit) -> Result
aws_sdk_path,
)
.context(here!())?;
let commit_hash = find_last_commit(aws_sdk_path).context(here!())?;
let commit_hash = GetLastCommit::new(aws_sdk_path).run().context(here!())?;
eprintln!("\tsuccessfully created mirror commit {}", commit_hash);
@ -462,19 +464,6 @@ where
Ok(())
}
fn find_last_commit(repo_path: &Path) -> Result<String> {
let output = Command::new("git")
.arg("rev-parse")
.arg("HEAD")
.current_dir(&repo_path)
.output()
.map_err(|err| anyhow!("couldn't get commit hash: {}", err))?;
let hash = String::from_utf8_lossy(&output.stdout);
Ok(hash.to_string())
}
/// For a slice containing `S` where `S: AsRef<OsStr>`, join all `S` into a space-separated String.
fn stringify_args<S>(args: &[S]) -> String
where
@ -484,10 +473,6 @@ where
args.join(" ")
}
fn _is_running_in_github_action() -> bool {
std::env::var("GITHUB_ACTIONS").unwrap_or_default() == "true"
}
fn is_a_git_repository(dir: &Path) -> bool {
dir.join(".git").is_dir()
}

91
tools/smithy-rs-tool-common/Cargo.lock generated Normal file
View File

@ -0,0 +1,91 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
[[package]]
name = "async-trait"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "smithy-rs-tool-common"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"tokio",
]
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "tokio"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"

View File

@ -0,0 +1,17 @@
[package]
name = "smithy-rs-tool-common"
version = "0.1.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
edition = "2021"
license = "Apache-2.0"
publish = false
[workspace]
[features]
async-shell = ["tokio"]
[dependencies]
anyhow = "1"
async-trait = "0.1"
tokio = { version = "1", features = ["rt", "macros"], optional = true }

View File

@ -0,0 +1,12 @@
#!/bin/bash
#
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.
#
# Run by CI to check the canary-lambda
set -e
cd "$(dirname $0)"
cargo test
cargo test --all-features
cargo clippy

View File

@ -0,0 +1,5 @@
#!/bin/bash
if [[ "$1" != "checkout" || "$2" != "test-revision" ]]; then
echo "wrong arguments" >&2
exit 1
fi

View File

@ -0,0 +1,6 @@
#!/bin/bash
if [[ "$1" != "rev-parse" || "$2" != "HEAD" ]]; then
echo "wrong arguments" >&2
exit 1
fi
echo "commithash"

View File

@ -0,0 +1,41 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
use anyhow::{bail, Result};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
mod checkout_revision;
pub use checkout_revision::CheckoutRevision;
mod get_current_tag;
pub use get_current_tag::GetCurrentTag;
mod get_last_commit;
pub use get_last_commit::GetLastCommit;
/// Attempts to find git repository root from the given location.
pub fn find_git_repository_root(repo_name: &str, location: impl AsRef<Path>) -> Result<PathBuf> {
let mut current_dir = location.as_ref().canonicalize()?.to_path_buf();
let os_name = OsStr::new(repo_name);
loop {
if is_git_root(&current_dir) {
if let Some(file_name) = current_dir.file_name() {
if os_name == file_name {
return Ok(current_dir);
}
}
break;
} else if !current_dir.pop() {
break;
}
}
bail!("failed to find {0} repository root", repo_name)
}
fn is_git_root(path: &Path) -> bool {
let path = path.join(".git");
path.exists() && path.is_dir()
}

View File

@ -0,0 +1,75 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::shell::{handle_failure, ShellOperation};
use anyhow::Result;
use std::path::PathBuf;
use std::process::Command;
pub struct CheckoutRevision {
program: &'static str,
path: PathBuf,
revision: String,
}
impl CheckoutRevision {
pub fn new(path: impl Into<PathBuf>, revision: impl Into<String>) -> Self {
CheckoutRevision {
program: "git",
path: path.into(),
revision: revision.into(),
}
}
}
impl ShellOperation for CheckoutRevision {
type Output = ();
fn run(&self) -> Result<()> {
let mut command = Command::new(self.program);
command.arg("checkout");
command.arg(&self.revision);
command.current_dir(&self.path);
let output = command.output()?;
handle_failure("checkout revision", &output)?;
Ok(())
}
}
#[cfg(all(test, not(target_os = "windows")))]
mod tests {
use super::*;
#[test]
fn checkout_revision_success() {
CheckoutRevision {
program: "./git_checkout_revision",
path: "./fake_git".into(),
revision: "test-revision".into(),
}
.run()
.unwrap();
}
#[test]
fn checkout_revision_failure() {
let result = CheckoutRevision {
program: "./git_fails",
path: "./fake_git".into(),
revision: "test-revision".into(),
}
.run();
assert!(result.is_err(), "expected error, got {:?}", result);
assert_eq!(
"Failed to checkout revision:\n\
Status: 1\n\
Stdout: some stdout failure message\n\n\
Stderr: some stderr failure message\n\n",
format!("{}", result.err().unwrap())
);
}
}

View File

@ -5,34 +5,33 @@
use crate::shell::{handle_failure, output_text, ShellOperation};
use anyhow::Result;
use async_trait::async_trait;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
pub struct GetCurrentTag<'a> {
pub struct GetCurrentTag {
program: &'static str,
path: &'a Path,
path: PathBuf,
}
impl<'a> GetCurrentTag<'a> {
pub fn new(path: &'a Path) -> GetCurrentTag<'a> {
impl GetCurrentTag {
pub fn new(path: impl Into<PathBuf>) -> GetCurrentTag {
GetCurrentTag {
program: "git",
path,
path: path.into(),
}
}
}
#[async_trait]
impl<'a> ShellOperation for GetCurrentTag<'a> {
impl ShellOperation for GetCurrentTag {
type Output = String;
async fn spawn(&self) -> Result<String> {
fn run(&self) -> Result<String> {
let mut command = Command::new(self.program);
command.arg("describe");
command.arg("--tags");
command.current_dir(self.path);
let output = tokio::task::spawn_blocking(move || command.output()).await??;
command.current_dir(&self.path);
let output = command.output()?;
handle_failure("get current tag", &output)?;
let (stdout, _) = output_text(&output);
Ok(stdout.trim().into())
@ -43,11 +42,23 @@ impl<'a> ShellOperation for GetCurrentTag<'a> {
mod tests {
use super::*;
#[tokio::test]
async fn get_current_tag_success() {
#[test]
fn get_current_tag_success() {
let tag = GetCurrentTag {
program: "./git_describe_tags",
path: "./fake_git".as_ref(),
path: "./fake_git".into(),
}
.run()
.unwrap();
assert_eq!("some-tag", tag);
}
#[cfg(feature = "async-shell")]
#[tokio::test]
async fn get_current_tag_success_async() {
let tag = GetCurrentTag {
program: "./git_describe_tags",
path: "./fake_git".into(),
}
.spawn()
.await
@ -55,14 +66,13 @@ mod tests {
assert_eq!("some-tag", tag);
}
#[tokio::test]
async fn get_current_tag_failure() {
#[test]
fn get_current_tag_failure() {
let result = GetCurrentTag {
program: "./git_fails",
path: "./fake_git".as_ref(),
path: "./fake_git".into(),
}
.spawn()
.await;
.run();
assert!(result.is_err(), "expected error, got {:?}", result);
assert_eq!(

View File

@ -0,0 +1,73 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
use crate::shell::{handle_failure, output_text, ShellOperation};
use anyhow::Result;
use std::path::PathBuf;
use std::process::Command;
pub struct GetLastCommit {
program: &'static str,
repo_path: PathBuf,
}
impl GetLastCommit {
pub fn new(repo_path: impl Into<PathBuf>) -> GetLastCommit {
GetLastCommit {
program: "git",
repo_path: repo_path.into(),
}
}
}
impl ShellOperation for GetLastCommit {
type Output = String;
fn run(&self) -> Result<String> {
let mut command = Command::new(self.program);
command.arg("rev-parse");
command.arg("HEAD");
command.current_dir(&self.repo_path);
let output = command.output()?;
handle_failure("get last commit", &output)?;
let (stdout, _) = output_text(&output);
Ok(stdout.trim().into())
}
}
#[cfg(all(test, not(target_os = "windows")))]
mod tests {
use super::*;
#[test]
fn get_last_commit_success() {
let last_commit = GetLastCommit {
program: "./git_revparse_head",
repo_path: "./fake_git".into(),
}
.run()
.unwrap();
assert_eq!("commithash", last_commit);
}
#[test]
fn get_last_commit_faijlure() {
let result = GetLastCommit {
program: "./git_fails",
repo_path: "./fake_git".into(),
}
.run();
assert!(result.is_err(), "expected error, got {:?}", result);
assert_eq!(
"Failed to get last commit:\n\
Status: 1\n\
Stdout: some stdout failure message\n\n\
Stderr: some stderr failure message\n\n",
format!("{}", result.err().unwrap())
);
}
}

View File

@ -0,0 +1,9 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
/// Returns `true` if this code is being run in GitHub Actions
pub fn running_in_github_actions() -> bool {
std::env::var("GITHUB_ACTIONS").unwrap_or_default() == "true"
}

View File

@ -0,0 +1,9 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
pub mod git;
pub mod github_actions;
pub mod package;
pub mod shell;

View File

@ -0,0 +1,35 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
pub const SMITHY_PREFIX: &str = "aws-smithy-";
pub const SDK_PREFIX: &str = "aws-sdk-";
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum PackageCategory {
SmithyRuntime,
AwsRuntime,
AwsSdk,
Unknown,
}
impl PackageCategory {
/// Returns true if the category is `AwsRuntime` or `AwsSdk`
pub fn is_sdk(&self) -> bool {
matches!(self, PackageCategory::AwsRuntime | PackageCategory::AwsSdk)
}
/// Categorizes a package based on its name
pub fn from_package_name(name: &str) -> PackageCategory {
if name.starts_with(SMITHY_PREFIX) {
PackageCategory::SmithyRuntime
} else if name.starts_with(SDK_PREFIX) {
PackageCategory::AwsSdk
} else if name.starts_with("aws-") {
PackageCategory::AwsRuntime
} else {
PackageCategory::Unknown
}
}
}

View File

@ -9,10 +9,19 @@ use std::process::Output;
#[async_trait]
pub trait ShellOperation {
type Output;
type Output: Send + 'static;
/// Runs the command synchronously.
fn run(&self) -> Result<Self::Output>;
/// Runs the command asynchronously.
async fn spawn(&self) -> Result<Self::Output>;
#[cfg(feature = "async-shell")]
async fn spawn(self) -> Result<Self::Output>
where
Self: Sized + 'static,
{
tokio::task::spawn_blocking(move || self.run()).await?
}
}
/// Returns (stdout, stderr)