Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to CLI v3 #304

Merged
merged 23 commits into from
Mar 4, 2023
Merged

Migrate to CLI v3 #304

merged 23 commits into from
Mar 4, 2023

Conversation

nebulatgs
Copy link
Contributor

The repository tracking changes can be found at https://github.com/railwayapp/cliv3

Summary of Changes

  • Migrate to using Rust with Clap for CLI args
  • Use the v2 Public API
  • Support for railway service to link to a service
  • railway domain to generate a service domain
  • Add various flags for use in CI/CD scripts

@nebulatgs nebulatgs added the release/major Author major release label Mar 3, 2023
@@ -1,58 +0,0 @@
name: Tag
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight nit: this comes from years of managing yml. I personally find .yml files unreadable whenever you have any yml file above 100 lines. There is some opportunity to maybe split the .ymls and steps, but I think this is fine for now and dependent on personal opinion.

Why do you think we should have merged the files in your opinion?

Cargo.toml Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
uninstall.sh Show resolved Hide resolved
src/config.rs Show resolved Hide resolved
src/config.rs Show resolved Hide resolved
Comment on lines +9 to +14
pub const NO_SERVICE_LINKED: &str =
"No service linked and no plugins found\nRun `railway service` to link a service";
pub const ABORTED_BY_USER: &str = "Aborted by user";
pub const PROJECT_NOT_FOUND: &str = "Project not found!";
pub const SERVICE_NOT_FOUND: &str = "Service not found!";
pub const NON_INTERACTIVE_FAILURE: &str = "This command is only available in interactive mode";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should these be a single error enum with a custom Display?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, is that more idiomatic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think would be a little bit. But very low priority in the grand scheme of things

src/entities.rs Outdated Show resolved Hide resolved
src/commands/link.rs Outdated Show resolved Hide resolved
src/commands/domain.rs Outdated Show resolved Hide resolved
src/commands/domain.rs Outdated Show resolved Hide resolved
src/commands/run.rs Show resolved Hide resolved
src/commands/starship.rs Show resolved Hide resolved
Copy link
Contributor

@JakeCooper JakeCooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say in general we need to take a pass over this to make it more clear and clean

I know you're trying to land this for today, so we can just fix the flags I've added for now, but we're gonna want to go over this again and iron it out because IMO, there are a few places for which it's a bit awkward

src/client.rs Outdated Show resolved Hide resolved
Comment on lines +66 to +70
} else {
inquire::MultiSelect::new("Select plugins to add", filtered_plugins)
.with_render_config(render_config)
.prompt()?
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove the else since you have a return in the if already

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we can, please try to keep code linear

Logic trees should be avoided if early returns will do (which they usually will)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't an early return, it's a conditional value (similar to a ternary expression in other languages), so the else is needed

Comment on lines +81 to +98
if !std::io::stdout().is_terminal() {
println!("Creating {}...", plugin);
post_graphql::<mutations::PluginCreate, _>(&client, configs.get_backboard(), vars)
.await?;
} else {
let spinner = indicatif::ProgressBar::new_spinner()
.with_style(
indicatif::ProgressStyle::default_spinner()
.tick_chars(TICK_STRING)
.template("{spinner:.green} {msg}")?,
)
.with_message(format!("Creating {plugin}..."));
spinner.enable_steady_tick(Duration::from_millis(100));
post_graphql::<mutations::PluginCreate, _>(&client, configs.get_backboard(), vars)
.await?;
spinner.finish_with_message(format!("Created {plugin}"));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no library which supports optional spinners for non tty environments?

My comment was going to be we can probably pull this into some sort of textual CLI library but there's gotta be one that supports both interactive as well as non interactive terminals

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just abstract it ourselves. We shouldn't have to duplicate the gql query every time depending on if we are in a terminal environment or not

Comment on lines +34 to +36
let service = if body.project.services.edges.len() == 1 {
body.project.services.edges[0].node.clone().id
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does line 35 return? If so, we should be using explicit returns

Implicit returns are fine for small functions like map/filter/return but should be avoided, for clarity, here

Also, if the else is going to bail, we should flip the branches and have an early return to remove the else case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This another instance of a conditional value

Comment on lines +66 to +103
if !std::io::stdout().is_terminal() {
println!("Creating domain...");

let res = post_graphql::<mutations::ServiceDomainCreate, _>(
&client,
configs.get_backboard(),
vars,
)
.await?;

let body = res.data.context("Failed to create service domain.")?;
let domain = body.service_domain_create.domain;

println!("Service Domain created: {}", domain.bold());
} else {
let spinner = indicatif::ProgressBar::new_spinner()
.with_style(
indicatif::ProgressStyle::default_spinner()
.tick_chars(TICK_STRING)
.template("{spinner:.green} {msg}")?,
)
.with_message("Creating domain...");
spinner.enable_steady_tick(Duration::from_millis(100));

let res = post_graphql::<mutations::ServiceDomainCreate, _>(
&client,
configs.get_backboard(),
vars,
)
.await?;

let body = res.data.context("Failed to create service domain.")?;
let domain = body.service_domain_create.domain;

spinner.finish_and_clear();

println!("Service Domain created: {}", domain.bold());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many cases here of duplicate logic related to the is_terminal() call with spinner and non spinner

I'd suggest either finding a CLI output library which supports this natively for both tty and non-tty, or compartmentalizing your logic into a library

Comment on lines 27 to 46
let name = inquire::Text::new("Project Name")
.with_formatter(&|s| {
if s.is_empty() {
"Will be randomly generated".to_string()
} else {
s.to_string()
}
})
.with_placeholder("my-first-project")
.with_help_message("Leave blank to generate a random name")
.with_render_config(render_config)
.prompt()?;

let name = if name.is_empty() {
use names::Generator;
let mut generator = Generator::default();
generator.next().context("Failed to generate name")?
} else {
name
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is kinda brutal to follow

Suggestion: replace it with a "prompt or generate" function which:

  • Prompts
  • Returns if value
  • returns a generated name

IMO much cleaner logical flow than the "Prompt, check, hoist-in-else" behavior. That last part I really think we should avoid. I'm still only 99% certain that's what it's doing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. In general I think we should abstract out the inquire library so we can do things like

let name = prompt("Project Name", "{placeholder}", "{help message}").or_else(|| random_name())

or some variation of that. Basically condensing these lines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great idea, will do!

Comment on lines 43 to 68
let environment = if let Some(environment_name_or_id) = args.environment {
let environment = body
.project
.environments
.edges
.iter()
.find(|env| {
env.node.name == environment_name_or_id || env.node.id == environment_name_or_id
})
.context("Environment not found")?;
ProjectEnvironment(&environment.node)
} else if !std::io::stdout().is_terminal() {
bail!("Environment must be provided when not running in a terminal");
} else {
inquire::Select::new(
"Select an environment",
body.project
.environments
.edges
.iter()
.map(|env| ProjectEnvironment(&env.node))
.collect(),
)
.with_render_config(configs.get_render_config())
.prompt()?
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic that probably would benefit from being pulled into a function

Comment on lines 71 to 78
let confirmed = !if std::io::stdout().is_terminal() {
inquire::Confirm::new("Are you sure you want to unlink this project?")
.with_render_config(configs.get_render_config())
.with_default(true)
.prompt()?
} else {
true
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else val type function here.

Generally, if you're using else, the code can be cleaned up

Copy link
Contributor

@ndneighbor ndneighbor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Green, expected fast follow on for code quality.

@ndneighbor ndneighbor merged commit 0cfb79d into master Mar 4, 2023
@ndneighbor ndneighbor deleted the nebula/v3-migration branch March 4, 2023 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release/major Author major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants