Skip to content

Commit 6e7349a

Browse files
jdisantircoh
andauthored
Improve publisher tool (#263)
* Fix issues with publish tool * Add `--continue-from` argument to publish * Fix batch sorting issue * Check if a package is published before publishing * Add ability to yank package versions in bulk * Disable incremental compilation to reduce disk space used * Don't wait for correct `aws-sigv4` version * Correct crate ownership during publish * Always correct crate ownership * replace cargo search with crates.io API * Publisher updates * Fix clippy lints against Rust 1.56.1 * Incorporate CR feedback Co-authored-by: Russell Cohen <rcoh@amazon.com>
1 parent c92a310 commit 6e7349a

19 files changed

+931
-114
lines changed

tools/publisher/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ publish = false
99

1010
[dependencies]
1111
anyhow = "1.0"
12+
async-trait = "0.1.51"
1213
cargo_toml = "0.10.1"
1314
clap = "2.33"
15+
crates_io_api = "0.7.3"
16+
lazy_static = "1"
1417
dialoguer = "0.8"
1518
num_cpus = "1.13"
19+
regex = "1.5.4"
1620
semver = "1.0"
1721
thiserror = "1.0"
1822
tokio = { version = "1.12", features = ["full"] }
1923
toml = { version = "0.5.8", features = ["preserve_order"] }
2024
tracing = "0.1.29"
21-
tracing-subscriber = "0.2.25"
25+
tracing-subscriber = "0.2.25"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
echo "some stdout failure message"
3+
>&2 echo "some stderr failure message"
4+
exit 1
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
>&2 echo " Updating crates.io index"
3+
echo "rcoh (Russell Cohen)"
4+
echo "github:awslabs:rust-sdk-owners (rust-sdk-owners)"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
echo 'error: crate version `0.0.22-alpha` is already uploaded'
3+
exit 1
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
# Fake `cargo search aws-sdk-dynamodb` for unit testing
3+
echo 'aws-sdk-dynamodb = "0.0.22-alpha" # AWS SDK for Amazon DynamoDB'
4+
echo 'aws-sdk-dynamodbstreams = "0.0.22-alpha" # AWS SDK for Amazon DynamoDB Streams'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
exit 0
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
{
3+
echo " Updating crates.io index"
4+
echo " Yank aws-sigv4:0.0.0"
5+
echo "error: failed to yank from the registry at https://crates.io"
6+
echo
7+
echo "Caused by:"
8+
echo ' the remote server responded with an error: crate `aws-sigv4` does not have a version `0.0.0`'
9+
} >&2
10+
exit 101

tools/publisher/src/cargo.rs

Lines changed: 53 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,82 +5,70 @@
55

66
//! Module for interacting with Cargo.
77
8+
mod add_owner;
9+
mod get_owners;
10+
mod publish;
11+
mod yank;
12+
13+
pub use add_owner::AddOwner;
14+
pub use get_owners::GetOwners;
15+
pub use publish::Publish;
16+
pub use yank::Yank;
17+
818
use anyhow::{Context, Result};
9-
use std::path::{Path, PathBuf};
19+
use async_trait::async_trait;
20+
use std::borrow::Cow;
1021
use std::process::{Command, Output};
1122

12-
macro_rules! cmd {
13-
[ $( $x:expr ),* ] => {
14-
{
15-
let mut cmd = Cmd::new();
16-
$(cmd.push($x);)*
17-
cmd
18-
}
19-
};
20-
}
23+
#[async_trait]
24+
pub trait CargoOperation {
25+
type Output;
2126

22-
/// Confirms that cargo exists on the path.
23-
pub async fn confirm_installed_on_path() -> Result<()> {
24-
cmd!["cargo", "--version"]
25-
.spawn()
26-
.await
27-
.context("cargo is not installed on the PATH")?;
28-
Ok(())
29-
}
27+
/// Runs the command asynchronously.
28+
async fn spawn(&self) -> Result<Self::Output>;
3029

31-
/// Returns a `Cmd` that, when spawned, will asynchronously run `cargo publish` in the given crate path.
32-
pub fn publish_task(crate_path: &Path) -> Cmd {
33-
cmd!["cargo", "publish"].working_dir(crate_path)
30+
/// Returns a plan string that can be output to the user to describe the command.
31+
fn plan(&self) -> Option<Cow<'static, str>>;
3432
}
3533

36-
#[derive(Default)]
37-
pub struct Cmd {
38-
parts: Vec<String>,
39-
working_dir: Option<PathBuf>,
34+
/// Confirms that cargo exists on the path.
35+
pub fn confirm_installed_on_path() -> Result<()> {
36+
handle_failure(
37+
"discover cargo version",
38+
&Command::new("cargo")
39+
.arg("version")
40+
.output()
41+
.context("cargo is not installed on the PATH")?,
42+
)
43+
.context("cargo is not installed on the PATH")
4044
}
4145

42-
impl Cmd {
43-
fn new() -> Cmd {
44-
Default::default()
45-
}
46-
47-
fn push(&mut self, part: impl Into<String>) {
48-
self.parts.push(part.into());
49-
}
50-
51-
fn working_dir(mut self, working_dir: impl AsRef<Path>) -> Self {
52-
self.working_dir = Some(working_dir.as_ref().into());
53-
self
54-
}
55-
56-
/// Returns a plan string that can be output to the user to describe the command.
57-
pub fn plan(&self) -> String {
58-
let mut plan = String::new();
59-
if let Some(working_dir) = &self.working_dir {
60-
plan.push_str(&format!("[in {:?}]: ", working_dir));
61-
}
62-
plan.push_str(&self.parts.join(" "));
63-
plan
64-
}
46+
/// Returns (stdout, stderr)
47+
fn output_text(output: &Output) -> (String, String) {
48+
(
49+
String::from_utf8_lossy(&output.stdout).to_string(),
50+
String::from_utf8_lossy(&output.stderr).to_string(),
51+
)
52+
}
6553

66-
/// Runs the command asynchronously.
67-
pub async fn spawn(mut self) -> Result<Output> {
68-
let working_dir = self
69-
.working_dir
70-
.take()
71-
.unwrap_or_else(|| std::env::current_dir().unwrap());
72-
let mut command: Command = self.into();
73-
tokio::task::spawn_blocking(move || Ok(command.current_dir(working_dir).output()?)).await?
54+
fn handle_failure(operation_name: &str, output: &Output) -> Result<(), anyhow::Error> {
55+
if !output.status.success() {
56+
return Err(capture_error(operation_name, output));
7457
}
58+
Ok(())
7559
}
7660

77-
impl From<Cmd> for Command {
78-
fn from(cmd: Cmd) -> Self {
79-
assert!(!cmd.parts.is_empty());
80-
let mut command = Command::new(&cmd.parts[0]);
81-
for i in 1..cmd.parts.len() {
82-
command.arg(&cmd.parts[i]);
83-
}
84-
command
85-
}
61+
fn capture_error(operation_name: &str, output: &Output) -> anyhow::Error {
62+
let message = format!(
63+
"Failed to {name}:\nStatus: {status}\nStdout: {stdout}\nStderr: {stderr}\n",
64+
name = operation_name,
65+
status = if let Some(code) = output.status.code() {
66+
format!("{}", code)
67+
} else {
68+
"Killed by signal".to_string()
69+
},
70+
stdout = String::from_utf8_lossy(&output.stdout),
71+
stderr = String::from_utf8_lossy(&output.stderr)
72+
);
73+
anyhow::Error::msg(message)
8674
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
use crate::cargo::{handle_failure, CargoOperation};
7+
use anyhow::Result;
8+
use async_trait::async_trait;
9+
use std::borrow::Cow;
10+
use std::process::Command;
11+
12+
pub struct AddOwner<'a> {
13+
program: &'static str,
14+
package_name: &'a str,
15+
owner: &'a str,
16+
}
17+
18+
impl<'a> AddOwner<'a> {
19+
pub fn new(package_name: &'a str, owner: &'a str) -> AddOwner<'a> {
20+
AddOwner {
21+
program: "cargo",
22+
package_name,
23+
owner,
24+
}
25+
}
26+
}
27+
28+
#[async_trait]
29+
impl<'a> CargoOperation for AddOwner<'a> {
30+
type Output = ();
31+
32+
async fn spawn(&self) -> Result<()> {
33+
let mut command = Command::new(self.program);
34+
command
35+
.arg("owner")
36+
.arg("--add")
37+
.arg(self.owner)
38+
.arg(self.package_name);
39+
let output = tokio::task::spawn_blocking(move || command.output()).await??;
40+
handle_failure("add owner", &output)?;
41+
Ok(())
42+
}
43+
44+
fn plan(&self) -> Option<Cow<'static, str>> {
45+
None
46+
}
47+
}
48+
49+
#[cfg(all(test, not(target_os = "windows")))]
50+
mod tests {
51+
use super::*;
52+
53+
#[tokio::test]
54+
async fn add_owner_success() {
55+
AddOwner {
56+
program: "./fake_cargo/cargo_success",
57+
package_name: "aws-sdk-s3",
58+
owner: "github:awslabs:rust-sdk-owners",
59+
}
60+
.spawn()
61+
.await
62+
.unwrap();
63+
}
64+
65+
#[tokio::test]
66+
async fn get_owners_failed() {
67+
let result = AddOwner {
68+
program: "./fake_cargo/cargo_fails",
69+
package_name: "aws-sdk-s3",
70+
owner: "github:awslabs:rust-sdk-owners",
71+
}
72+
.spawn()
73+
.await;
74+
75+
assert!(result.is_err(), "expected error, got {:?}", result);
76+
assert_eq!(
77+
"Failed to add owner:\n\
78+
Status: 1\n\
79+
Stdout: some stdout failure message\n\n\
80+
Stderr: some stderr failure message\n\n",
81+
format!("{}", result.err().unwrap())
82+
);
83+
}
84+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
use crate::cargo::{handle_failure, output_text, CargoOperation};
7+
use anyhow::Result;
8+
use async_trait::async_trait;
9+
use regex::Regex;
10+
use std::borrow::Cow;
11+
use std::process::Command;
12+
13+
pub struct GetOwners<'a> {
14+
program: &'static str,
15+
package_name: &'a str,
16+
}
17+
18+
impl<'a> GetOwners<'a> {
19+
pub fn new(package_name: &'a str) -> GetOwners<'a> {
20+
GetOwners {
21+
program: "cargo",
22+
package_name,
23+
}
24+
}
25+
}
26+
27+
#[async_trait]
28+
impl<'a> CargoOperation for GetOwners<'a> {
29+
type Output = Vec<String>;
30+
31+
async fn spawn(&self) -> Result<Vec<String>> {
32+
let mut command = Command::new(self.program);
33+
command.arg("owner").arg("--list").arg(self.package_name);
34+
let output = tokio::task::spawn_blocking(move || command.output()).await??;
35+
handle_failure("get crate owners", &output)?;
36+
37+
let mut result = Vec::new();
38+
let (stdout, _) = output_text(&output);
39+
let line_re = Regex::new(r#"^([\w\d\-_:]+)\s+\([\w\d\s\-_]+\)$"#).unwrap();
40+
for line in stdout.lines() {
41+
if let Some(captures) = line_re.captures(line) {
42+
let user_id = captures.get(1).unwrap().as_str();
43+
result.push(user_id.to_string());
44+
} else {
45+
return Err(anyhow::Error::msg(format!(
46+
"unrecognized line in `cargo owner` output: {}",
47+
line
48+
)));
49+
}
50+
}
51+
Ok(result)
52+
}
53+
54+
fn plan(&self) -> Option<Cow<'static, str>> {
55+
None
56+
}
57+
}
58+
59+
#[cfg(all(test, not(target_os = "windows")))]
60+
mod tests {
61+
use super::*;
62+
63+
#[tokio::test]
64+
async fn get_owners_success() {
65+
let owners = GetOwners {
66+
program: "./fake_cargo/cargo_owner_list",
67+
package_name: "aws-sdk-s3",
68+
}
69+
.spawn()
70+
.await
71+
.unwrap();
72+
assert_eq!(
73+
vec![
74+
"rcoh".to_string(),
75+
"github:awslabs:rust-sdk-owners".to_string()
76+
],
77+
owners
78+
);
79+
}
80+
81+
#[tokio::test]
82+
async fn get_owners_failed() {
83+
let result = GetOwners {
84+
program: "./fake_cargo/cargo_fails",
85+
package_name: "aws-sdk-s3",
86+
}
87+
.spawn()
88+
.await;
89+
90+
assert!(result.is_err(), "expected error, got {:?}", result);
91+
assert_eq!(
92+
"Failed to get crate owners:\n\
93+
Status: 1\n\
94+
Stdout: some stdout failure message\n\n\
95+
Stderr: some stderr failure message\n\n",
96+
format!("{}", result.err().unwrap())
97+
);
98+
}
99+
}

0 commit comments

Comments
 (0)