Skip to content

Commit 73cbb62

Browse files
committed
fix(cargo-rustapi): fix CI docs job rustdoc error
Escape brackets in the argv[1] doc comment so rustdoc no longer treats it as a broken intra-doc link under RUSTDOCFLAGS=-D warnings.
1 parent fe90a10 commit 73cbb62

7 files changed

Lines changed: 234 additions & 61 deletions

File tree

crates/cargo-rustapi/src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ enum Commands {
8787
/// Logout from RustAPI Cloud
8888
Logout(LogoutArgs),
8989

90+
/// List your RustAPI Cloud deploys
91+
#[cfg(feature = "cloud")]
92+
Deploys,
93+
9094
/// Deploy to various platforms
9195
#[command(subcommand)]
9296
Deploy(DeployArgs),
@@ -118,6 +122,8 @@ impl Cli {
118122
Commands::Login(args) => commands::login(args).await,
119123
Commands::Whoami(args) => commands::whoami(args).await,
120124
Commands::Logout(args) => commands::logout(args).await,
125+
#[cfg(feature = "cloud")]
126+
Commands::Deploys => commands::deploys_list().await,
121127
Commands::Deploy(args) => commands::deploy(args).await,
122128
#[cfg(feature = "replay")]
123129
Commands::Replay(args) => commands::replay(args).await,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//! RustAPI Cloud HTTP helpers (auth refresh, shared client).
2+
3+
use anyhow::{anyhow, Context, Result};
4+
use reqwest::Client;
5+
use serde::Deserialize;
6+
use std::time::Duration;
7+
8+
use crate::config::{load_config, save_config, CloudConfig};
9+
10+
#[derive(Deserialize)]
11+
struct RefreshResponse {
12+
access_token: String,
13+
refresh_token: String,
14+
#[allow(dead_code)]
15+
token_type: String,
16+
#[allow(dead_code)]
17+
expires_in: u64,
18+
}
19+
20+
#[derive(Deserialize)]
21+
struct RefreshError {
22+
error: String,
23+
#[serde(default)]
24+
error_description: Option<String>,
25+
}
26+
27+
pub fn cloud_http_client() -> Result<Client> {
28+
Client::builder()
29+
.timeout(Duration::from_secs(30))
30+
.build()
31+
.context("Failed to build HTTP client")
32+
}
33+
34+
/// Load config and return a valid access token, refreshing when the API returns 401.
35+
pub async fn with_access_token<F, Fut, T>(mut operation: F) -> Result<T>
36+
where
37+
F: FnMut(Client, String, String) -> Fut,
38+
Fut: std::future::Future<Output = Result<T>>,
39+
{
40+
let mut config = load_config()?;
41+
let cloud_url = config
42+
.cloud_url
43+
.clone()
44+
.unwrap_or_else(|| "https://api.rustapi.cloud".into());
45+
let token = config
46+
.token
47+
.clone()
48+
.ok_or_else(|| anyhow!("Not logged in. Run `cargo rustapi login` first."))?;
49+
50+
let client = cloud_http_client()?;
51+
match operation(client.clone(), cloud_url.clone(), token.clone()).await {
52+
Ok(value) => Ok(value),
53+
Err(err) if looks_like_auth_failure(&err) => {
54+
refresh_tokens(&client, &cloud_url, &mut config).await?;
55+
let new_token = config.token.clone().expect("token after refresh");
56+
operation(client, cloud_url, new_token).await
57+
}
58+
Err(err) => Err(err),
59+
}
60+
}
61+
62+
fn looks_like_auth_failure(err: &anyhow::Error) -> bool {
63+
let msg = err.to_string().to_lowercase();
64+
msg.contains("401")
65+
|| msg.contains("unauthorized")
66+
|| msg.contains("invalid or expired token")
67+
|| msg.contains("invalid/expired token")
68+
}
69+
70+
pub async fn refresh_tokens(
71+
client: &Client,
72+
cloud_url: &str,
73+
config: &mut CloudConfig,
74+
) -> Result<()> {
75+
let refresh = config
76+
.refresh_token
77+
.clone()
78+
.ok_or_else(|| anyhow!("Session expired. Run `cargo rustapi login` again."))?;
79+
80+
let resp = client
81+
.post(format!("{}/auth/refresh", cloud_url.trim_end_matches('/')))
82+
.json(&serde_json::json!({ "refresh_token": refresh }))
83+
.send()
84+
.await
85+
.context("Failed to connect for token refresh")?;
86+
87+
if !resp.status().is_success() {
88+
let body: RefreshError = resp.json().await.unwrap_or(RefreshError {
89+
error: "invalid_grant".into(),
90+
error_description: Some("Refresh failed".into()),
91+
});
92+
return Err(anyhow!(
93+
"{}: {}",
94+
body.error,
95+
body.error_description.unwrap_or_default()
96+
));
97+
}
98+
99+
let body: RefreshResponse = resp.json().await.context("Invalid refresh response")?;
100+
config.token = Some(body.access_token);
101+
config.refresh_token = Some(body.refresh_token);
102+
save_config(config)?;
103+
Ok(())
104+
}

crates/cargo-rustapi/src/commands/deploy.rs

Lines changed: 45 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -685,18 +685,7 @@ fn cloud_binary_path(project_name: &str, target: Option<&str>) -> PathBuf {
685685

686686
#[cfg(feature = "cloud")]
687687
async fn deploy_cloud(args: CloudArgs) -> Result<()> {
688-
let config = load_config()?;
689-
690-
let token = config.token.as_ref().ok_or_else(|| {
691-
anyhow::anyhow!("Not logged in. Run `cargo rustapi login` or `cargo-rustapi login` first.")
692-
})?;
693-
694-
let cloud_url = config
695-
.cloud_url
696-
.as_deref()
697-
.unwrap_or("https://api.rustapi.cloud")
698-
.trim_end_matches('/');
699-
688+
let _config = load_config()?;
700689
let project_name = args
701690
.name
702691
.unwrap_or_else(|| get_package_name().unwrap_or_else(|_| "rustapi-app".to_string()));
@@ -747,31 +736,39 @@ async fn deploy_cloud(args: CloudArgs) -> Result<()> {
747736
println!(" 📤 Uploading {:.1} MB...", upload_mb);
748737
}
749738

750-
let client = cloud_upload_client(binary_data.len())?;
751-
let form = reqwest::multipart::Form::new()
752-
.text("project_name", project_name.clone())
753-
.part(
754-
"binary",
755-
reqwest::multipart::Part::bytes(binary_data).file_name(format!("{}.bin", project_name)),
756-
);
757-
758-
let deploy_resp: DeployResponse = client
759-
.post(format!("{}/deploy", cloud_url))
760-
.header("Authorization", format!("Bearer {}", token))
761-
.multipart(form)
762-
.send()
763-
.await
764-
.with_context(|| {
765-
format!(
766-
"Failed to upload to RustAPI Cloud ({cloud_url}/deploy). \
767-
If the connection timed out, check your upload speed or try again."
768-
)
769-
})?
770-
.json()
771-
.await
772-
.context("Invalid response from deploy endpoint")?;
739+
let deploy_id = crate::cloud::with_access_token(|_client, cloud_url, token| {
740+
let project_name = project_name.clone();
741+
let binary_data = binary_data.clone();
742+
async move {
743+
let upload_client = cloud_upload_client(binary_data.len())?;
744+
let form = reqwest::multipart::Form::new()
745+
.text("project_name", project_name.clone())
746+
.part(
747+
"binary",
748+
reqwest::multipart::Part::bytes(binary_data)
749+
.file_name(format!("{}.bin", project_name)),
750+
);
773751

774-
let deploy_id = deploy_resp.deploy_id;
752+
let deploy_resp: DeployResponse = upload_client
753+
.post(format!("{}/deploy", cloud_url.trim_end_matches('/')))
754+
.header("Authorization", format!("Bearer {}", token))
755+
.multipart(form)
756+
.send()
757+
.await
758+
.with_context(|| {
759+
format!(
760+
"Failed to upload to RustAPI Cloud ({cloud_url}/deploy). \
761+
If the connection timed out, check your upload speed or try again."
762+
)
763+
})?
764+
.json()
765+
.await
766+
.context("Invalid response from deploy endpoint")?;
767+
768+
Ok(deploy_resp.deploy_id)
769+
}
770+
})
771+
.await?;
775772

776773
if args.no_wait {
777774
println!(" ✅ Deploy queued: {}", deploy_id);
@@ -788,7 +785,12 @@ async fn deploy_cloud(args: CloudArgs) -> Result<()> {
788785
tokio::time::sleep(Duration::from_secs(3)).await;
789786

790787
let status_resp: DeployResponse =
791-
match fetch_deploy_status(cloud_url, token, &deploy_id).await {
788+
match crate::cloud::with_access_token(|client, base, token| {
789+
let deploy_id = deploy_id.clone();
790+
async move { fetch_deploy_status(&client, &base, &token, &deploy_id).await }
791+
})
792+
.await
793+
{
792794
Ok(body) => body,
793795
Err(_) => continue,
794796
};
@@ -819,14 +821,6 @@ async fn deploy_cloud(args: CloudArgs) -> Result<()> {
819821
Ok(())
820822
}
821823

822-
#[cfg(feature = "cloud")]
823-
fn cloud_http_client() -> Result<reqwest::Client> {
824-
reqwest::Client::builder()
825-
.timeout(Duration::from_secs(30))
826-
.build()
827-
.context("Failed to build HTTP client")
828-
}
829-
830824
#[cfg(feature = "cloud")]
831825
fn upload_timeout_secs(binary_bytes: usize) -> u64 {
832826
// ~13 MB needs more than 10s on typical home uplinks; scale with payload size.
@@ -846,11 +840,11 @@ fn cloud_upload_client(binary_bytes: usize) -> Result<reqwest::Client> {
846840

847841
#[cfg(feature = "cloud")]
848842
async fn fetch_deploy_status(
843+
client: &reqwest::Client,
849844
cloud_url: &str,
850845
token: &str,
851846
deploy_id: &str,
852847
) -> Result<DeployResponse> {
853-
let client = cloud_http_client()?;
854848
let response = client
855849
.get(format!("{}/deploy/{}/status", cloud_url, deploy_id))
856850
.header("Authorization", format!("Bearer {}", token))
@@ -904,19 +898,11 @@ mod status_tests {
904898

905899
#[cfg(feature = "cloud")]
906900
async fn deploy_status(args: DeployStatusArgs) -> Result<()> {
907-
let config = load_config()?;
908-
909-
let token = config.token.as_ref().ok_or_else(|| {
910-
anyhow::anyhow!("Not logged in. Run `cargo rustapi login` or `cargo-rustapi login` first.")
911-
})?;
912-
913-
let cloud_url = config
914-
.cloud_url
915-
.as_deref()
916-
.unwrap_or("https://api.rustapi.cloud")
917-
.trim_end_matches('/');
918-
919-
let status = fetch_deploy_status(cloud_url, token, &args.deploy_id).await?;
901+
let status = crate::cloud::with_access_token(|client, cloud_url, token| {
902+
let deploy_id = args.deploy_id.clone();
903+
async move { fetch_deploy_status(&client, &cloud_url, &token, &deploy_id).await }
904+
})
905+
.await?;
920906

921907
println!(" Deploy ID: {}", status.deploy_id);
922908
println!(" Status: {}", status.status);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use anyhow::Context;
2+
use serde::Deserialize;
3+
4+
use crate::cloud;
5+
6+
#[derive(Deserialize)]
7+
struct DeployListItem {
8+
id: String,
9+
project_name: String,
10+
status: String,
11+
url: Option<String>,
12+
created_at: String,
13+
}
14+
15+
pub async fn deploys_list() -> anyhow::Result<()> {
16+
cloud::with_access_token(|client, cloud_url, token| async move {
17+
let response = client
18+
.get(format!("{}/deploys", cloud_url.trim_end_matches('/')))
19+
.header("Authorization", format!("Bearer {}", token))
20+
.send()
21+
.await
22+
.context("Failed to fetch deploys")?;
23+
24+
if !response.status().is_success() {
25+
let status = response.status();
26+
let body = response.text().await.unwrap_or_default();
27+
return Err(anyhow::anyhow!("Deploy list failed ({}): {}", status, body));
28+
}
29+
30+
let items: Vec<DeployListItem> = response
31+
.json()
32+
.await
33+
.context("Invalid deploy list response")?;
34+
35+
if items.is_empty() {
36+
println!("No deploys yet. Run `cargo rustapi deploy cloud` from a project.");
37+
return Ok(());
38+
}
39+
40+
println!("{:<36} {:<18} {:<10} URL", "PROJECT", "DEPLOY ID", "STATUS");
41+
println!("{}", "-".repeat(100));
42+
43+
for item in items {
44+
println!(
45+
"{:<36} {:<18} {:<10} {}",
46+
truncate(&item.project_name, 36),
47+
truncate(&item.id, 18),
48+
item.status,
49+
item.url.as_deref().unwrap_or("—")
50+
);
51+
println!(" deployed: {}", item.created_at);
52+
}
53+
54+
Ok(())
55+
})
56+
.await
57+
}
58+
59+
fn truncate(value: &str, max: usize) -> String {
60+
if value.chars().count() <= max {
61+
value.to_string()
62+
} else {
63+
format!(
64+
"{}…",
65+
value
66+
.chars()
67+
.take(max.saturating_sub(1))
68+
.collect::<String>()
69+
)
70+
}
71+
}

crates/cargo-rustapi/src/commands/login.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async fn fetch_user_info(
188188

189189
Ok(UserInfo {
190190
login: v["login"].as_str().unwrap_or("unknown").into(),
191-
tier: v["tier"].as_str().unwrap_or("hobby").into(),
191+
tier: v["tier"].as_str().unwrap_or("unlimited").into(),
192192
avatar_url: v["avatar_url"].as_str().map(String::from),
193193
})
194194
}

crates/cargo-rustapi/src/commands/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod add;
44
mod bench;
55
mod client;
66
mod deploy;
7+
#[cfg(feature = "cloud")]
8+
mod deploys;
79
mod docs;
810
mod doctor;
911
mod generate;
@@ -21,6 +23,8 @@ pub use add::{add, AddArgs};
2123
pub use bench::{bench, BenchArgs};
2224
pub use client::{client, ClientArgs};
2325
pub use deploy::{deploy, DeployArgs};
26+
#[cfg(feature = "cloud")]
27+
pub use deploys::deploys_list;
2428
pub use docs::open_docs;
2529
pub use doctor::{doctor, DoctorArgs};
2630
pub use generate::{generate, GenerateArgs};

crates/cargo-rustapi/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! Provides project scaffolding and development utilities for RustAPI.
44
55
mod cli;
6+
#[cfg(feature = "cloud")]
7+
mod cloud;
68
mod commands;
79
mod config;
810
mod templates;
@@ -28,7 +30,7 @@ async fn main() -> anyhow::Result<()> {
2830
cli.execute().await
2931
}
3032

31-
/// Strip `rustapi` when `cargo rustapi <cmd>` forwards it as argv[1].
33+
/// Strip `rustapi` when `cargo rustapi <cmd>` forwards it as argv\[1\].
3234
pub(crate) fn strip_cargo_forwarded_subcommand(
3335
mut args: Vec<std::ffi::OsString>,
3436
) -> Vec<std::ffi::OsString> {

0 commit comments

Comments
 (0)