Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
126 changes: 41 additions & 85 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -38,127 +39,82 @@ 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<Config, ActionError> {
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 {
format!("{}{}", self.tag_prefix, self.default_version)
}
}

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<String, ActionError> {
env::var(var).map_err(|_| ActionError::MissingEnv(var))
}

fn get_github_token() -> Result<String, ActionError> {
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<String, ActionError> {
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<VersionIncrementStrategy, ActionError> {
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::<Vec<&str>>();
(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<String, ActionError> {
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<bool, ActionError> {
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<bool, ActionError> {
let v = require_env(DRY_RUN)?;
Ok(matches!(v.to_ascii_lowercase().as_str(), "true"))
}
24 changes: 24 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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}"),
}
}
}
39 changes: 27 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
73 changes: 29 additions & 44 deletions src/releases.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<TagName> {
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<TagName> = 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<Option<TagName>, 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<TagName> {
pub fn create_release(&self, tag: &Semver) -> Result<Option<TagName>, ActionError> {
let req = CreateReleaseRequest {
tag_name: tag.to_string(),
target_commitish: self.config.commitish.clone(),
Expand All @@ -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<TagName> = 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())),
}
}
}
Loading