From 6bdad44cc6c77c564414fb1f4c77e94c9bdef0c7 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:15:39 -0500 Subject: [PATCH 01/12] Interrogate list_releases result more closely --- src/api/release.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/api/release.rs b/src/api/release.rs index 09f925a..27f2aad 100644 --- a/src/api/release.rs +++ b/src/api/release.rs @@ -23,15 +23,26 @@ pub async fn list_releases( let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/"); let req = client.get(request_url).send().await; let response = req.map_err(|reqwest_err| crate::Error::WrappedReqwestErr(reqwest_err))?; - let release_list = response - .json::>() - .await - .map_err(|reqwest_err| { - // Convert reqwest errors to my own - // TODO: Create all error variants (see lib.rs) - crate::Error::WrappedReqwestErr(reqwest_err) - })?; - return Ok(release_list); + if response.status().is_success() { + let release_list = response + .json::>() + .await + .map_err(|reqwest_err| { + // Convert reqwest errors to my own + // TODO: Create all error variants (see lib.rs) + crate::Error::WrappedReqwestErr(reqwest_err) + })?; + return Ok(release_list); + } else if response.status().is_client_error() { + let mesg = response + .json::() + .await + .map_err(|reqwest_err| { + crate::Error::WrappedReqwestErr(reqwest_err) + })?; + return Err(crate::Error::ApiErrorMessage(mesg)); + } + panic!("Reached end of list_releases without matching a return pathway."); } #[derive(Debug, Deserialize, Serialize)] From 96e9ff4ce6b5a4fb625b105f3e36cb242d98faf0 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:22:20 -0500 Subject: [PATCH 02/12] Interrogate create_release result more closely --- src/api/release.rs | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/api/release.rs b/src/api/release.rs index 27f2aad..d13b1d3 100644 --- a/src/api/release.rs +++ b/src/api/release.rs @@ -45,14 +45,6 @@ pub async fn list_releases( panic!("Reached end of list_releases without matching a return pathway."); } -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -enum CreateResult { - Success(structs::release::Release), - ErrWithMessage(ApiError), - Empty, -} - pub async fn create_release( client: &reqwest::Client, gitea_url: &str, @@ -60,27 +52,28 @@ pub async fn create_release( submission: CreateReleaseOption, ) -> Result { let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases"); - let req = client + let response = client .post(request_url) .json(&submission) .send() .await .map_err(|e| crate::Error::from(e))?; - let new_release = req - .json::() - .await - .map_err(|e| crate::Error::from(e))?; - match new_release { - CreateResult::Success(release) => Ok(release), - CreateResult::ErrWithMessage(api_error) => { - if api_error.message == "token is required" { - Err(crate::Error::MissingAuthToken) - } else { - Err(crate::Error::ApiErrorMessage(api_error)) - } - } - CreateResult::Empty => panic!("How can we have 200 OK and no release info? No. Crash"), + if response.status().is_success() { + let new_release = response + .json::() + .await + .map_err(|e| crate::Error::from(e))?; + return Ok(new_release); + } else if response.status().is_client_error() { + let mesg = response + .json::() + .await + .map_err(|reqwest_err| { + crate::Error::WrappedReqwestErr(reqwest_err) + })?; + return Err(crate::Error::ApiErrorMessage(mesg)) } + panic!("Reached end of create_release without matching a return path"); } pub fn edit_release(id: u64) -> Result { todo!(); From d42cbbc1ec9347da8b7c64c8596910d3b13f133a Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:22:48 -0500 Subject: [PATCH 03/12] Drop unused imports --- src/api/release.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/release.rs b/src/api/release.rs index d13b1d3..474c991 100644 --- a/src/api/release.rs +++ b/src/api/release.rs @@ -1,9 +1,7 @@ -use serde::{Deserialize, Serialize}; use crate::{ ApiError, Result, structs::{ - self, release::{CreateReleaseOption, Release}, }, }; From 8a11c21b736c03657a82494e76e03a8fb1d16a05 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:24:16 -0500 Subject: [PATCH 04/12] "Fix" the test case I can't meaningfully unit test these things like this. I'll explore creating a tarball of a known Gitea configuration and using Docker to test against that. For now, just... kinda keep the test building. --- src/api/release_attachment.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index 9af71bf..9005dc0 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -103,11 +103,12 @@ mod tests { .await; let api_err = api_result.expect_err("Received Ok(()) after uploading non-existent file. That's nonsense, the API Function is wrong."); match api_err { - crate::Error::Placeholder => panic!("Received dummy response from the API function. Finish implementing it, stupid"), - crate::Error::WrappedReqwestErr(error) => panic!("Received a reqwest::Error from the API function: {error}"), - crate::Error::MissingAuthToken => unreachable!("Missing auth token... in a unit test that already panics without the auth token..."), - crate::Error::NoSuchFile => (), // test passes - crate::Error::ApiErrorMessage(api_error) => panic!("Received an error message from the API: {api_error:?}"), + crate::Error::Placeholder=>panic!("Received dummy response from the API function. Finish implementing it, stupid"), + crate::Error::WrappedReqwestErr(error)=>panic!("Received a reqwest::Error from the API function: {error}"), + crate::Error::MissingAuthToken=>unreachable!("Missing auth token... in a unit test that already panics without the auth token..."), + crate::Error::NoSuchFile=>(), + crate::Error::ApiErrorMessage(api_error)=>panic!("Received an error message from the API: {api_error:?}"), + crate::Error::NoSuchRelease => todo!(), } } From 0c70b584ba87d560fe6dc36d6e6ee5a9755590aa Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:30:56 -0500 Subject: [PATCH 05/12] Interrogate create_release_attachment result --- src/api/release_attachment.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index 9005dc0..fb9e072 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -1,5 +1,7 @@ use std::fs; +use crate::ApiError; + pub fn check_release_match_repo() {} pub fn get_release_attachment() {} pub fn list_release_attachments() { @@ -34,12 +36,23 @@ pub async fn create_release_attachment( let form = reqwest::multipart::Form::new().part("attachment", data); - let request = client + let response = client .post(&request_url) .multipart(form) .query(&[("name", file.split("/").last())]) .send() .await?; + if response.status().is_success() { + // TODO: create a struct Attachment and return it to the caller. + } else if response.status().is_client_error() { + let mesg = response + .json::() + .await + .map_err(|reqwest_err| { + crate::Error::WrappedReqwestErr(reqwest_err) + })?; + return Err(crate::Error::ApiErrorMessage(mesg)); + } } Ok(()) } From 4a0addda67ef4d1df783f440de29af5f326f261e Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:40:58 -0500 Subject: [PATCH 06/12] Fold client-error-decode into a util function --- src/api/release.rs | 14 ++------------ src/api/release_attachment.rs | 7 +------ src/lib.rs | 9 +++++++++ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/api/release.rs b/src/api/release.rs index 474c991..b9d3290 100644 --- a/src/api/release.rs +++ b/src/api/release.rs @@ -32,12 +32,7 @@ pub async fn list_releases( })?; return Ok(release_list); } else if response.status().is_client_error() { - let mesg = response - .json::() - .await - .map_err(|reqwest_err| { - crate::Error::WrappedReqwestErr(reqwest_err) - })?; + let mesg = crate::decode_client_error(response).await?; return Err(crate::Error::ApiErrorMessage(mesg)); } panic!("Reached end of list_releases without matching a return pathway."); @@ -63,12 +58,7 @@ pub async fn create_release( .map_err(|e| crate::Error::from(e))?; return Ok(new_release); } else if response.status().is_client_error() { - let mesg = response - .json::() - .await - .map_err(|reqwest_err| { - crate::Error::WrappedReqwestErr(reqwest_err) - })?; + let mesg = crate::decode_client_error(response).await?; return Err(crate::Error::ApiErrorMessage(mesg)) } panic!("Reached end of create_release without matching a return path"); diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index fb9e072..0a3def3 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -45,12 +45,7 @@ pub async fn create_release_attachment( if response.status().is_success() { // TODO: create a struct Attachment and return it to the caller. } else if response.status().is_client_error() { - let mesg = response - .json::() - .await - .map_err(|reqwest_err| { - crate::Error::WrappedReqwestErr(reqwest_err) - })?; + let mesg = crate::decode_client_error(response).await?; return Err(crate::Error::ApiErrorMessage(mesg)); } } diff --git a/src/lib.rs b/src/lib.rs index 14cfab7..42cac0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,15 @@ pub struct ApiError { url: String, } +pub (crate) async fn decode_client_error(response: reqwest::Response) -> Result { + response + .json::() + .await + .map_err(|reqwest_err| { + crate::Error::WrappedReqwestErr(reqwest_err) + }) +} + #[derive(Debug)] pub enum Error { Placeholder, // TODO: Enumerate error modes From a5f6335b5f8e88205cfef4248e1d96d795a9be08 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:50:16 -0500 Subject: [PATCH 07/12] Add `Attachment` struct, new iface for create-rel The Attachment struct exists, but this makes it glaringly obvious that I've made a bad interface. The create_release_attachment should only accept one file at a time and the loop over all files should happen in main.rs I've changed the signature, removed the loops, and wired in the newer error handling routines. Needs fixing at call sites. --- src/api/release_attachment.rs | 64 +++++++++++++++++------------------ src/structs/mod.rs | 13 +++++++ 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index 0a3def3..7918178 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -1,6 +1,6 @@ use std::fs; -use crate::ApiError; +use crate::structs::Attachment; pub fn check_release_match_repo() {} pub fn get_release_attachment() {} @@ -12,44 +12,44 @@ pub async fn create_release_attachment( gitea_url: &str, repo: &str, release_id: usize, - files: Vec, -) -> crate::Result<()> { + file: String, +) -> crate::Result { let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets"); - // Ensure all files exists before starting the uploads - for file in &files { - match fs::exists(file) { - Ok(true) => continue, - Ok(false) => return Err(crate::Error::NoSuchFile), - Err(e) => { - eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); - panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); - }, - } + match fs::exists(&file) { + Ok(true) => (), + Ok(false) => return Err(crate::Error::NoSuchFile), + Err(e) => { + eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); + panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); + }, } - for file in files { - println!("Uploading file {}", &file); - let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap()) - .file_name("attachment") - .mime_str("text/plain")?; + println!("Uploading file {}", &file); + let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap()) + .file_name("attachment") + .mime_str("text/plain")?; - let form = reqwest::multipart::Form::new().part("attachment", data); + let form = reqwest::multipart::Form::new().part("attachment", data); - let response = client - .post(&request_url) - .multipart(form) - .query(&[("name", file.split("/").last())]) - .send() - .await?; - if response.status().is_success() { - // TODO: create a struct Attachment and return it to the caller. - } else if response.status().is_client_error() { - let mesg = crate::decode_client_error(response).await?; - return Err(crate::Error::ApiErrorMessage(mesg)); - } + let response = client + .post(&request_url) + .multipart(form) + .query(&[("name", file.split("/").last())]) + .send() + .await?; + if response.status().is_success() { + // TODO: create a struct Attachment and return it to the caller. + let attachment_desc = response + .json::() + .await + .map_err(|e| crate::Error::from(e))?; + return Ok(attachment_desc); + } else if response.status().is_client_error() { + let mesg = crate::decode_client_error(response).await?; + return Err(crate::Error::ApiErrorMessage(mesg)); } - Ok(()) + panic!("Reached end of release_attachment without matching a return path"); } pub fn edit_release_attachment() {} pub fn delete_release_attachment() {} diff --git a/src/structs/mod.rs b/src/structs/mod.rs index 357eccf..5623965 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -1,2 +1,15 @@ +use serde::{Deserialize, Serialize}; + pub mod release; pub mod repo; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Attachment { + id: usize, + name: String, + size: i64, + download_count: i64, + created: String, // TODO: Date-time struct + uuid: String, + download_url: String, +} From 06795df3f74b9a806b62b22766a527359e4f84be Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sat, 7 Jun 2025 23:57:19 -0500 Subject: [PATCH 08/12] Update main.rs to use new attachment iface --- src/main.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 02bd869..650be33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::fs; + use gt_tool::cli::Args; use gt_tool::structs::release::{CreateReleaseOption, Release}; @@ -78,14 +80,26 @@ async fn main() -> Result<(), gt_tool::Error> { gt_tool::api::release::list_releases(&client, &args.gitea_url, &args.repo).await?; if let Some(release) = match_release_by_tag(&tag_name, release_candidates) { - gt_tool::api::release_attachment::create_release_attachment( - &client, - &args.gitea_url, - &args.repo, - release.id, - files, - ) - .await?; + for file in &files { + match fs::exists(file) { + Ok(true) => continue, + Ok(false) => return Err(gt_tool::Error::NoSuchFile), + Err(e) => { + eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); + panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); + }, + } + } + for file in files { + let _attach_desc = gt_tool::api::release_attachment::create_release_attachment( + &client, + &args.gitea_url, + &args.repo, + release.id, + file, + ) + .await?; + } } else { println!("ERR: Couldn't find a release matching the tag \"{tag_name}\"."); return Err(gt_tool::Error::NoSuchRelease); From 8246337ae4203609ace17d7465d8b2e2adc12968 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sun, 8 Jun 2025 00:06:59 -0500 Subject: [PATCH 09/12] Delete the unit tests They aren't useful anyway. --- src/api/release_attachment.rs | 137 ---------------------------------- 1 file changed, 137 deletions(-) diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index 7918178..d4fda6f 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -53,140 +53,3 @@ pub async fn create_release_attachment( } pub fn edit_release_attachment() {} pub fn delete_release_attachment() {} - -#[cfg(test)] -mod tests { - use reqwest::header::{self, ACCEPT}; - - use crate::structs::release::Release; - - - #[tokio::test] - async fn attach_file_exists() { - let conf = TestConfig::new(); - let release_candidates = - crate::api::release::list_releases( - &conf.client, - &conf.server, - &conf.repo - ) - .await - .expect("Failed to get releases. Pre-conditions unmet, aborting test!"); - - let release = match_release_by_tag(&conf.release_tag, release_candidates) - .expect("Failed to select matching release. Pre-conditions unmet, aborting test!"); - - let api_result = super::create_release_attachment( - &conf.client, - &conf.server, - &conf.repo, - release.id, - vec![String::from("Cargo.toml")], - ) - .await; - } - - #[tokio::test] - async fn attach_file_missing() { - let conf = TestConfig::new(); - let release_candidates = - crate::api::release::list_releases( - &conf.client, - &conf.server, - &conf.repo - ) - .await - .expect("Failed to get releases. Pre-conditions unmet, aborting test!"); - - let release = match_release_by_tag(&conf.release_tag, release_candidates) - .expect("Failed to select matching release. Pre-conditions unmet, aborting test!"); - - let api_result = super::create_release_attachment( - &conf.client, - &conf.server, - &conf.repo, - release.id, - vec![String::from("./this-file-doesnt-exist")], - ) - .await; - let api_err = api_result.expect_err("Received Ok(()) after uploading non-existent file. That's nonsense, the API Function is wrong."); - match api_err { - crate::Error::Placeholder=>panic!("Received dummy response from the API function. Finish implementing it, stupid"), - crate::Error::WrappedReqwestErr(error)=>panic!("Received a reqwest::Error from the API function: {error}"), - crate::Error::MissingAuthToken=>unreachable!("Missing auth token... in a unit test that already panics without the auth token..."), - crate::Error::NoSuchFile=>(), - crate::Error::ApiErrorMessage(api_error)=>panic!("Received an error message from the API: {api_error:?}"), - crate::Error::NoSuchRelease => todo!(), - } - } - - struct TestConfig { - server: String, - repo: String, - release_tag: String, - client: reqwest::Client, - } - - impl TestConfig { - fn new() -> Self { - let server = std::env::var("TEST_GITEA_SERVER") - .expect("Must set server address in env var \"TEST_GITEA_SERVER\""); - let repo = std::env::var("TEST_GITEA_REPO") - .expect("Must set / name in env var \"TEST_GITEA_REPO\""); - let token = format!( - "token {}", - std::env::var("TEST_GITEA_KEY") - .expect("Must set the API token in env var \"TEST_GITEA_KEY\"") - ); - let release_tag = std::env::var("TEST_GITEA_RELEASE_TAG") - .expect("Must set the target release tag in env var \"TEST_GITEA_RELEASE_TAG\""); - - let mut headers = reqwest::header::HeaderMap::new(); - headers.append(ACCEPT, header::HeaderValue::from_static("application/json")); - headers.append("Authorization", token.parse().unwrap()); - - let client = reqwest::Client::builder() - .user_agent(format!( - "gt-tools-autotest-agent{}", - env!("CARGO_PKG_VERSION") - )) - .default_headers(headers) - .build() - .expect("Failed to build reqwest::Client."); - - return Self { - server, - repo, - release_tag, - client - }; - } - } - - // Testing utils - fn match_release_by_tag(tag: &String, releases: Vec) -> Option { - let mut release: Option = None; - for rel in releases { - if rel.tag_name == *tag { - // Only store the value if one hasn't been stored already - if let Some(first_release) = &release { - // if there was already a match, begin the error diagnostic creation. - let first_id = first_release.id; - let second_id = rel.id; - assert!( - first_id != second_id, - "FAILURE: Found the same release ID twice while scanning for duplicate tags. How did we get the same one twice?" - ); - eprintln!("ERROR: Two releases have been found for the tag \"{tag}\"."); - eprintln!("ERROR: first ID: {first_id}"); - eprintln!("ERROR: second ID: {second_id}"); - panic!("ERROR: Nonsense detected, I'm bailing out!"); - } else { - // else, store our first (and hopefully only) match - release = Some(rel); - } - } - } - return release; - } -} \ No newline at end of file From b200785e7172d91225396f8d3662b7880a6c75c1 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sun, 8 Jun 2025 10:40:54 -0500 Subject: [PATCH 10/12] ... and the unit testing notes in README.md --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 47d2718..37688c9 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,3 @@ One of these, defaults to `help`: | upload-release | Uploads one-or-more files to an existing release, identified by it's tag name. | | help | prints the help text (the usage summary above). | -## Unit Testing - -The unit test~~s~~ require a Gitea server to execute against. This information is supplied by environment variables rather than on the command line, but it is otherwise exactly the same usage. - -| Variable | Description | -|-|-| -| TEST_GITEA_SERVER | Server URL, match `-u`, `--url` | -| TEST_GITEA_REPO | Owner + repo name, match `-u` `--repo` | -| TEST_GITEA_KEY | API key, match `RELEASE_KEY_GITEA`. The use of a new variable for the API token is to help avoid accidentally touching a production environment during test execution. | -| TEST_GITEA_RELEASE_TAG | Git tag used to identify the Release. Same as `upload-release`'s positional argument ``. | - From 88cafc096f58958469b1b5ea6116100ae57f2b61 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sun, 8 Jun 2025 10:43:32 -0500 Subject: [PATCH 11/12] Drop unused import in api/release.rs --- src/api/release.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/release.rs b/src/api/release.rs index b9d3290..4250d3f 100644 --- a/src/api/release.rs +++ b/src/api/release.rs @@ -1,6 +1,6 @@ use crate::{ - ApiError, Result, + Result, structs::{ release::{CreateReleaseOption, Release}, }, From a0ba8e7ea8a09be4a2eeb66606bb3653efadf530 Mon Sep 17 00:00:00 2001 From: Robert Garrett Date: Sun, 1 Jun 2025 18:01:21 -0500 Subject: [PATCH 12/12] Use pre Rust 1.81 compatible file-exists test The function `std::fs::exists(...)` was stabilized in Rust 1.81, which means it can't be used in the Debian Bookworm build. This patch swaps to a compatible implementation leaning on the std::path::Path struct. I'm both "upstreaming" a Debian-specific patch I had to make for the package, and fixing the additional usage now in `main.rs`. There doesn't seem to be any compelling reason to avoid using this function, so I figure I should merge it into the base release. --- src/api/release_attachment.rs | 5 +++-- src/main.rs | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index d4fda6f..ae626ef 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{fs, path}; use crate::structs::Attachment; @@ -16,7 +16,8 @@ pub async fn create_release_attachment( ) -> crate::Result { let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets"); - match fs::exists(&file) { + let path = path::Path::new(&file); + match path.try_exists() { Ok(true) => (), Ok(false) => return Err(crate::Error::NoSuchFile), Err(e) => { diff --git a/src/main.rs b/src/main.rs index 650be33..555b843 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -use std::fs; + +use std::path; use gt_tool::cli::Args; use gt_tool::structs::release::{CreateReleaseOption, Release}; @@ -81,7 +82,8 @@ async fn main() -> Result<(), gt_tool::Error> { if let Some(release) = match_release_by_tag(&tag_name, release_candidates) { for file in &files { - match fs::exists(file) { + let path = path::Path::new(&file); + match path.try_exists() { Ok(true) => continue, Ok(false) => return Err(gt_tool::Error::NoSuchFile), Err(e) => {