Skip to content

Commit

Permalink
Override with CLI and remove required -r for --repository-url
Browse files Browse the repository at this point in the history
  • Loading branch information
cnpryer committed Feb 25, 2024
1 parent be8424e commit 6617479
Showing 1 changed file with 88 additions and 74 deletions.
162 changes: 88 additions & 74 deletions rye/src/cli/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub struct Args {
#[arg(short, long, default_value = DEFAULT_REPOSITORY)]
repository: String,
/// The repository url to publish to.
#[arg(long, requires = "repository")]
#[arg(long)]
repository_url: Option<Url>,
/// The username to authenticate to the repository with.
#[arg(short, long)]
Expand Down Expand Up @@ -68,29 +68,22 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
bail!("virtual packages cannot be published");
}

let repository_config = resolve_repository_config(&cmd)?;

// Get the files to publish.
let files = match cmd.dist {
Some(paths) => paths,
None => vec![project.workspace_path().join("dist").join("*")],
};

let repository_config = resolve_repository_config(
cmd.username,
cmd.token.map(Secret::new),
&cmd.repository,
cmd.repository_url,
&cmd.skip_save_credentials,
&cmd.yes,
)?;

let mut publish_cmd = Command::new(get_venv_python_bin(&venv));

// Build Twine command
publish_cmd
.arg("-mtwine")
.arg("--no-color")
.arg("--non-interactive")
.arg("upload")
.arg("--non-interactive")
.args(files);

// If a username is provided use it, if a password is provided without a username then use __token__ with Twine.
Expand Down Expand Up @@ -130,126 +123,147 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
Ok(())
}

#[derive(Debug)]
struct RepositoryConfig {
username: Option<String>,
password: Option<Secret<String>>,
repository_url: Option<Url>,
}

// 1. Get username/token/password from arguments
// 1. Or get username/token/password from ~/.rye/credentials keyed by provided repository
// - Prompt decrypt if not --yes
// 1. Or get username/token/password from prompt (if not --yes)
// 1. Otherwise let Twine handle remaining credentials resolution
// 1. Offer encryption if not --skip-save-credentials
//
// We try to do our best to preserve a 'pypi' defaults
fn resolve_repository_config(
username: Option<String>,
password: Option<Secret<String>>,
repository: &str,
repository_url: Option<Url>,
skip_save_credentials: &bool,
yes: &bool,
) -> Result<RepositoryConfig, Error> {
let mut repository_url = repository_url;
/// Resolve repository configuration needed for publishing packages.
///
/// 1. Resolve any configuration from ~/.rye/credentials using '--repository' (default is 'pypi')
/// 2. Override any resolved configuration with CLI.
/// 3. Prompt user for missing options (skips with '--yes').
/// 4. Finally, if no token is found, we can fallback to (username, none, url) for keyring backends.
///
/// We try to do our best to preserve a 'pypi' defaults. If --skip-save-credentials isn't provided
/// Rye will save the resolved credentials for '--repository' in ~/.rye/credentials.
fn resolve_repository_config(cmd: &Args) -> Result<RepositoryConfig, Error> {
let mut config = RepositoryConfig {
username: None,
password: None,
repository_url: None,
};

let mut rye_credentials = get_credentials()?;
let repo_credentials = rye_credentials
.entry(repository)
.entry(&cmd.repository)
.or_insert(Item::Table(Table::new()));

// Attempt to resolve url from credentials if we aren't provided with one.
if repository_url.is_none() {
if let Some(url) = repo_credentials
.get("repository-url")
.map(Item::to_string)
.map(escape_string)
{
repository_url = Some(Url::parse(&url)?);
}
if let Some(url) = repo_credentials
.get("repository-url")
.map(Item::to_string)
.map(escape_string)
{
config.repository_url = Some(Url::parse(&url)?);
}

if cmd.repository_url.is_some() {
config.repository_url = cmd.repository_url.clone();
}

// Make sure 'pypi' is configured correctly.
if repository == DEFAULT_REPOSITORY
&& repository_url
if cmd.repository == DEFAULT_REPOSITORY
&& config
.repository_url
.as_ref()
.is_some_and(|it| it.domain() != Some(DEFAULT_REPOSITORY_DOMAIN))
{
bail!(
"invalid pypi url {} (use -h for help)",
// Can always expect a repository url if condition is met.
repository_url.expect("matching pypi domain")
config.repository_url.expect("matching pypi domain")
);
}

let username = username.or(repo_credentials
.get("username")
.map(Item::to_string)
.map(escape_string));

let mut passphrase = Secret::new(String::new());
let mut has_prompted_decryption = false;

let mut password = password;

if password.is_none() {
if let Some(it) = repo_credentials
.get("token")
.map(Item::to_string)
.map(escape_string)
.map(Secret::new)
{
if !yes {
passphrase = prompt_decrypt_passphrase()?;
has_prompted_decryption = true;
}
password = Some(decrypt(&it, &passphrase)?);
if cmd.token.is_some() {
config.password = cmd.token.clone().map(Secret::new);
} else if let Some(it) = repo_credentials
.get("token")
.map(Item::to_string)
.map(escape_string)
.map(Secret::new)
{
if !cmd.yes {
passphrase = prompt_decrypt_passphrase()?;
has_prompted_decryption = true;
}
config.password = Some(decrypt(&it, &passphrase)?);
}

if password.is_none() && repository == DEFAULT_REPOSITORY && !yes {
if config.password.is_none() && cmd.repository == DEFAULT_REPOSITORY && !cmd.yes {
echo!("No access token found, generate one at: https://pypi.org/manage/account/token/");
let token = Secret::new(prompt_for_token()?);
password = Some(token)
config.password = Some(Secret::new(prompt_token()?));
};

if !skip_save_credentials {
if let Some(it) = password.as_ref() {
config.username = repo_credentials
.get("username")
.map(Item::to_string)
.map(escape_string);

if cmd.username.is_some() {
config.username = cmd.username.clone();
}

// If no token/password is resolved we can fallback to keyring with username and url.
if config.password.is_none() && config.username.is_none() {
if config.repository_url.is_none() {
bail!("no configuration was found for '{}'", cmd.repository);
}

if !cmd.yes {
config.username = Some(prompt_username()?);
}
}

if !cmd.skip_save_credentials {
if let Some(it) = config.password.as_ref() {
// Encryption step can be skipped using an empty passphrase with --yes form cli
if !yes && !has_prompted_decryption {
if !cmd.yes && !has_prompted_decryption {
passphrase = prompt_encrypt_passphrase()?;
}

let maybe_encrypted = encrypt(it, passphrase)?;
let maybe_encoded = maybe_encode(it, &maybe_encrypted);
repo_credentials["token"] = Item::Value(maybe_encoded.expose_secret().into());

if !maybe_encoded.expose_secret().is_empty() {
repo_credentials["token"] = Item::Value(maybe_encoded.expose_secret().into());
}
}

if let Some(usr) = username.as_ref() {
if let Some(usr) = config.username.as_ref() {
repo_credentials["username"] = Item::Value(usr.clone().into());
}

if let Some(url) = repository_url.as_ref() {
if let Some(url) = config.repository_url.as_ref() {
repo_credentials["repository-url"] = Item::Value(url.to_string().into());
}

write_credentials(&rye_credentials)?;
}

Ok(RepositoryConfig {
username,
password,
repository_url,
})
Ok(config)
}

fn prompt_for_token() -> Result<String, Error> {
fn prompt_token() -> Result<String, Error> {
eprint!("Access token: ");
let token = get_trimmed_user_input().context("failed to read provided token")?;

Ok(token)
}

fn prompt_username() -> Result<String, Error> {
eprint!("Username: ");
let username = get_trimmed_user_input().context("failed to read provided username")?;

Ok(username)
}

fn encrypt(secret: &Secret<String>, phrase: Secret<String>) -> Result<Secret<Vec<u8>>, Error> {
let token = if phrase.expose_secret().is_empty() {
secret.expose_secret().as_bytes().to_vec()
Expand Down

0 comments on commit 6617479

Please sign in to comment.