diff --git a/README.md b/README.md index 69f93ea..fb99011 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Release on Merge Action Source -[![CI](https://github.com/dexwritescode/release-on-merge-action-source/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/dexwritescode/release-on-merge-action-source/actions/workflows/ci.yaml) +[![CI](https://github.com/dexwritescode/release-on-merge-action-source/actions/workflows/ci.yaml/badge.svg)](https://github.com/dexwritescode/release-on-merge-action-source/actions/workflows/ci.yaml) Github action to create Github release on merge diff --git a/src/config.rs b/src/config.rs index 2defb07..86af3c7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ -use std::{env, fmt, process::exit, str::FromStr}; +use std::{env, fmt, str::FromStr}; +use crate::error::ActionError; use crate::semver::VersionIncrementStrategy; const INITIAL_VERSION: &str = "INPUT_INITIAL-VERSION"; @@ -38,29 +39,23 @@ impl fmt::Debug for Token { } } -impl Default for Config { - fn default() -> Self { - Self::new() - } -} - impl Config { - pub fn new() -> Config { - let (owner, repo) = get_repo_info(); - Config { - github_output_path: get_github_output_path(), - github_token: Token(get_github_token()), + pub fn new() -> Result { + let (owner, repo) = get_repo_info()?; + Ok(Config { + github_output_path: get_github_output_path()?, + github_token: Token(get_github_token()?), github_host: get_github_host(), - increment_strategy: get_version_increment_strategy(), + increment_strategy: get_version_increment_strategy()?, default_version: get_default_version(), tag_prefix: get_tag_prefix(), repo, owner, - commitish: get_commitish(), + commitish: get_commitish()?, body: get_body(), - generate_release_notes: get_generate_release_notes(), - dry_run: is_dry_run(), - } + generate_release_notes: get_generate_release_notes()?, + dry_run: is_dry_run()?, + }) } pub fn get_default_tag(&self) -> String { @@ -68,97 +63,58 @@ impl Config { } } -fn get_github_token() -> String { - env::var(GITHUB_TOKEN).unwrap_or_else(|e| { - eprintln!("Could not read {}", GITHUB_TOKEN); - eprintln!("Error {}", e); - exit(1); - }) +fn require_env(var: &'static str) -> Result { + env::var(var).map_err(|_| ActionError::MissingEnv(var)) +} + +fn get_github_token() -> Result { + require_env(GITHUB_TOKEN) } -fn get_github_output_path() -> String { - env::var(GITHUB_OUTPUT).unwrap_or_else(|e| { - eprintln!("Could not read {}", GITHUB_OUTPUT); - eprintln!("Error {}", e); - exit(1); - }) +fn get_github_output_path() -> Result { + require_env(GITHUB_OUTPUT) } -fn get_version_increment_strategy() -> VersionIncrementStrategy { - env::var(INCREMENT_STRATEGY).map_or_else( - |e| { - eprintln!("Could not read {}", INCREMENT_STRATEGY); - eprintln!("Error {}", e); - exit(1); - }, - |value| { - VersionIncrementStrategy::from_str(&value).map_or_else( - |_| { - eprintln!("Invalid version-increment-strategy value: {}", value); - exit(1); - }, - |vis| vis, - ) - }, - ) +fn get_version_increment_strategy() -> Result { + let value = require_env(INCREMENT_STRATEGY)?; + VersionIncrementStrategy::from_str(&value) + .map_err(|_| ActionError::InvalidStrategy(value)) } fn get_default_version() -> String { - env::var(INITIAL_VERSION).unwrap_or("0.1.0".to_string()) + env::var(INITIAL_VERSION).unwrap_or_else(|_| "0.1.0".to_string()) } -fn get_repo_info() -> (String, String) { - env::var(GITHUB_REPOSITORY).map_or_else( - |e| { - eprintln!("Could not read {}", GITHUB_REPOSITORY); - eprintln!("Error {}", e); - exit(1); - }, - |v| { - let repo_info = v.split('/').collect::>(); - (repo_info[0].to_owned(), repo_info[1].to_owned()) - }, - ) +fn get_repo_info() -> Result<(String, String), ActionError> { + let v = require_env(GITHUB_REPOSITORY)?; + let mut parts = v.splitn(2, '/'); + let owner = parts.next().unwrap_or("").to_owned(); + let repo = parts.next().unwrap_or("").to_owned(); + Ok((owner, repo)) } fn get_tag_prefix() -> String { - env::var(TAG_PREFIX).unwrap_or("v".to_string()) + env::var(TAG_PREFIX).unwrap_or_else(|_| "v".to_string()) } fn get_github_host() -> String { - env::var(GITHUB_HOST).unwrap_or("https://api.github.com".to_string()) + env::var(GITHUB_HOST).unwrap_or_else(|_| "https://api.github.com".to_string()) } -fn get_commitish() -> String { - env::var(COMMITISH).unwrap_or_else(|e| { - eprintln!("Could not read {}", COMMITISH); - eprintln!("Error {}", e); - exit(1); - }) +fn get_commitish() -> Result { + require_env(COMMITISH) } fn get_body() -> String { env::var(BODY).unwrap_or_default() } -fn get_generate_release_notes() -> bool { - env::var(GENERATE_RELEASE_NOTES).map_or_else( - |e| { - eprintln!("Could not read {}", GENERATE_RELEASE_NOTES); - eprintln!("Error {}", e); - exit(1); - }, - |v| matches!(v.to_ascii_lowercase().as_str(), "true"), - ) +fn get_generate_release_notes() -> Result { + let v = require_env(GENERATE_RELEASE_NOTES)?; + Ok(matches!(v.to_ascii_lowercase().as_str(), "true")) } -fn is_dry_run() -> bool { - env::var(DRY_RUN).map_or_else( - |e| { - eprintln!("Could not read {}", DRY_RUN); - eprintln!("Error {}", e); - exit(1); - }, - |v| matches!(v.to_ascii_lowercase().as_str(), "true"), - ) +fn is_dry_run() -> Result { + let v = require_env(DRY_RUN)?; + Ok(matches!(v.to_ascii_lowercase().as_str(), "true")) } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..90166da --- /dev/null +++ b/src/error.rs @@ -0,0 +1,24 @@ +use std::fmt; + +#[derive(Debug)] +pub enum ActionError { + MissingEnv(&'static str), + InvalidStrategy(String), + InvalidTag(String), + Unauthorized, + ApiError(String), + UnexpectedStatus(u16), +} + +impl fmt::Display for ActionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ActionError::MissingEnv(var) => write!(f, "Required environment variable not set: {var}"), + ActionError::InvalidStrategy(s) => write!(f, "Invalid version-increment-strategy: {s}"), + ActionError::InvalidTag(s) => write!(f, "Could not parse tag as semver: {s}"), + ActionError::Unauthorized => write!(f, "Unauthorized — check your GITHUB_TOKEN"), + ActionError::ApiError(msg) => write!(f, "GitHub API error: {msg}"), + ActionError::UnexpectedStatus(code) => write!(f, "Unexpected HTTP status: {code}"), + } + } +} diff --git a/src/main.rs b/src/main.rs index 3b822d5..ab411de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ use std::{process::exit, str::FromStr}; pub mod config; use config::Config; +pub mod error; +use error::ActionError; pub mod github_client; use github_client::GithubClient; pub mod semver; @@ -12,37 +14,50 @@ use releases::Releases; use crate::semver::VersionIncrementStrategy; pub mod writer; -fn main() { - let config = Config::new(); +fn run() -> Result<(), ActionError> { + let config = Config::new()?; eprintln!("Config: {:?}", &config); if config.increment_strategy == VersionIncrementStrategy::NoRelease { eprintln!("Increment strategy NoRelease - exiting"); - exit(0); + return Ok(()); } let github_client = GithubClient::new(&config); let releases = Releases::new(&config, github_client); - let latest_release = releases.get_latest_release(); + + let latest_release = releases.get_latest_release()?; eprintln!("Retrieved release {:?}", &latest_release); - let default_tag = Semver::from_str(&config.get_default_tag()).unwrap(); - let new_tag = latest_release.map_or(default_tag, |v| { - Semver::from_str(&v.tag_name) - .unwrap() - .increment(&config.increment_strategy) - }); + let default_tag = Semver::from_str(&config.get_default_tag()) + .map_err(|_| ActionError::InvalidTag(config.get_default_tag()))?; + + let new_tag = match latest_release { + None => default_tag, + Some(v) => Semver::from_str(&v.tag_name) + .map_err(|_| ActionError::InvalidTag(v.tag_name.clone()))? + .increment(&config.increment_strategy), + }; eprintln!("Incremented version {}", &new_tag); if config.dry_run { eprintln!("Dry run mode active. Not creating a new release."); } else { eprintln!("Creating new release."); - let release = releases.create_release(&new_tag); + let release = releases.create_release(&new_tag)?; eprintln!("New release created {:?}", &release); - }; + } let mut w = writer::Writer::new(&config.github_output_path); w.write("version", &new_tag.get_version()); w.write("tag", &new_tag.to_string()); + + Ok(()) +} + +fn main() { + if let Err(e) = run() { + eprintln!("Error: {e}"); + exit(1); + } } diff --git a/src/releases.rs b/src/releases.rs index cc72f24..3d4437a 100644 --- a/src/releases.rs +++ b/src/releases.rs @@ -1,3 +1,4 @@ +use crate::error::ActionError; use crate::github_client::models::CreateReleaseRequest; use crate::github_client::models::TagName; use crate::github_client::GithubClient; @@ -6,41 +7,33 @@ use crate::Semver; use reqwest::StatusCode; -use std::process::exit; - pub struct Releases<'config> { config: &'config Config, client: GithubClient, } impl Releases<'_> { - pub fn new(config: &Config, client: GithubClient) -> Releases { + pub fn new(config: &Config, client: GithubClient) -> Releases<'_> { Releases { config, client } } - pub fn get_latest_release(&self) -> Option { - let latest_release_reponse = self.client.get_latest_release().unwrap_or_else(|e| { - eprintln!("Error getting the latest release {:?}", &e.to_string()); - exit(1); - }); - - let latest_release: Option = match latest_release_reponse.status() { - StatusCode::OK => latest_release_reponse.json().unwrap(), - StatusCode::NOT_FOUND => None, - StatusCode::UNAUTHORIZED => { - eprintln!("Unauthorized. Make sure you are using a valid token."); - exit(1); - } - s => { - eprintln!("Received response status {}", s); - exit(1); - } - }; - - latest_release + pub fn get_latest_release(&self) -> Result, ActionError> { + let response = self + .client + .get_latest_release() + .map_err(|e| ActionError::ApiError(e.to_string()))?; + + match response.status() { + StatusCode::OK => response + .json() + .map_err(|e| ActionError::ApiError(e.to_string())), + StatusCode::NOT_FOUND => Ok(None), + StatusCode::UNAUTHORIZED => Err(ActionError::Unauthorized), + s => Err(ActionError::UnexpectedStatus(s.as_u16())), + } } - pub fn create_release(&self, tag: &Semver) -> Option { + pub fn create_release(&self, tag: &Semver) -> Result, ActionError> { let req = CreateReleaseRequest { tag_name: tag.to_string(), target_commitish: self.config.commitish.clone(), @@ -51,25 +44,17 @@ impl Releases<'_> { generate_release_notes: self.config.generate_release_notes, }; - let response = self.client.create_release(&req).unwrap_or_else(|e| { - eprintln!("Error creating release {:?}", &req); - eprintln!("Error {}", e); - exit(1); - }); - - let release: Option = match response.status() { - StatusCode::CREATED => response.json().unwrap(), - StatusCode::UNAUTHORIZED => { - eprintln!("Unauthorized. Make sure you are using a valid token."); - exit(1); - } - s => { - eprintln!("Received response status {}", s); - eprintln!("{:?}", &response); - exit(1); - } - }; - - release + let response = self + .client + .create_release(&req) + .map_err(|e| ActionError::ApiError(e.to_string()))?; + + match response.status() { + StatusCode::CREATED => response + .json() + .map_err(|e| ActionError::ApiError(e.to_string())), + StatusCode::UNAUTHORIZED => Err(ActionError::Unauthorized), + s => Err(ActionError::UnexpectedStatus(s.as_u16())), + } } }