cargo_version_info/
github.rs1use anyhow::{
4 Context,
5 Result,
6};
7
8use crate::version::{
9 format_version,
10 increment_patch,
11 parse_version,
12};
13
14pub async fn get_latest_release_version(
19 owner: &str,
20 repo: &str,
21 github_token: Option<&str>,
22) -> Result<Option<String>> {
23 if let Some(token) = github_token
25 && let Ok(version) = get_latest_release_via_api(owner, repo, token).await
26 {
27 return Ok(Some(version));
28 }
29
30 get_latest_release_via_cli(owner, repo)
32}
33
34async fn get_latest_release_via_api(owner: &str, repo: &str, token: &str) -> Result<String> {
36 let octocrab = octocrab::OctocrabBuilder::new()
37 .personal_token(token.to_string())
38 .build()
39 .context("Failed to create GitHub API client")?;
40
41 let releases = octocrab
42 .repos(owner, repo)
43 .releases()
44 .list()
45 .per_page(1)
46 .send()
47 .await
48 .context("Failed to query GitHub releases")?;
49
50 let release = releases.items.first().context("No releases found")?;
51
52 let tag_name = release.tag_name.as_str();
53 let version = tag_name.strip_prefix('v').unwrap_or(tag_name);
54 let version = version.strip_prefix('V').unwrap_or(version);
55
56 Ok(version.to_string())
57}
58
59fn get_latest_release_via_cli(owner: &str, repo: &str) -> Result<Option<String>> {
61 use std::process::Command;
62
63 let output = Command::new("gh")
64 .args([
65 "release",
66 "list",
67 "--repo",
68 &format!("{}/{}", owner, repo),
69 "--exclude-drafts",
70 "--limit",
71 "1",
72 "--json",
73 "tagName",
74 "--jq",
75 ".[0].tagName",
76 ])
77 .output()
78 .context("Failed to execute gh CLI. Is it installed?")?;
79
80 if !output.status.success() {
81 let stderr = String::from_utf8_lossy(&output.stderr);
82 if stderr.contains("no releases found") || stderr.is_empty() {
83 return Ok(None);
84 }
85 anyhow::bail!("gh CLI failed: {}", stderr);
86 }
87
88 let tag_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
89 if tag_name.is_empty() {
90 return Ok(None);
91 }
92
93 let version = tag_name.strip_prefix('v').unwrap_or(&tag_name);
94 let version = version.strip_prefix('V').unwrap_or(version);
95
96 Ok(Some(version.to_string()))
97}
98
99pub async fn calculate_next_version(
101 owner: &str,
102 repo: &str,
103 github_token: Option<&str>,
104) -> Result<(String, String)> {
105 let latest_version_str = match get_latest_release_version(owner, repo, github_token).await? {
107 Some(v) => v,
108 None => {
109 return Ok(("0.0.0".to_string(), "0.0.1".to_string()));
111 }
112 };
113
114 let (major, minor, patch) = parse_version(&latest_version_str)
115 .with_context(|| format!("Failed to parse latest version: {}", latest_version_str))?;
116
117 let (major, minor, patch) = increment_patch(major, minor, patch);
118 let next_version = format_version(major, minor, patch);
119
120 Ok((latest_version_str, next_version))
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[tokio::test]
128 #[ignore] async fn test_get_latest_release_via_cli() {
130 if let Ok(Some(version)) = get_latest_release_via_cli("rust-lang", "rust") {
133 println!("Latest rust release: {}", version);
134 }
135 }
136}