#![allow(warnings)]
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "derive")]
pub use action_derive::Action;
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Input<'a> {
pub description: Option<&'a str>,
pub deprecation_message: Option<&'a str>,
pub default: Option<&'a str>,
pub required: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
pub enum LogLevel {
Debug,
Error,
Warning,
Notice,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
LogLevel::Debug => write!(f, "debug"),
LogLevel::Error => write!(f, "error"),
LogLevel::Warning => write!(f, "warning"),
LogLevel::Notice => write!(f, "notice"),
}
}
}
pub fn input_env_var(name: impl Into<String>) -> String {
let mut var: String = name.into();
if !var.starts_with("INPUT_") {
var = format!("INPUT_{var}");
}
var = var.replace(' ', "_").to_uppercase();
var
}
pub mod env {
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct Env(pub HashMap<String, String>);
impl FromIterator<(String, String)> for Env {
fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
Self::new(HashMap::from_iter(iter))
}
}
impl Env {
#[must_use]
pub fn new(values: HashMap<String, String>) -> Self {
let inner = values
.into_iter()
.map(|(k, v)| (super::input_env_var(k), v))
.collect();
Self(inner)
}
#[cfg(feature = "serde")]
pub fn from_reader(reader: impl std::io::Read) -> Result<Self, serde_yaml::Error> {
Ok(Self::new(serde_yaml::from_reader(reader)?))
}
}
#[cfg(feature = "serde")]
impl std::str::FromStr for Env {
type Err = serde_yaml::Error;
fn from_str(env: &str) -> Result<Self, Self::Err> {
Ok(Self::new(serde_yaml::from_str(env)?))
}
}
impl std::borrow::Borrow<HashMap<String, String>> for Env {
fn borrow(&self) -> &HashMap<String, String> {
&self.0
}
}
impl std::ops::Deref for Env {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Env {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub trait Read {
fn get(&self, key: &str) -> Result<String, std::env::VarError>;
}
pub trait Write {
fn set(&mut self, key: String, value: String);
}
impl<T> Read for T
where
T: std::borrow::Borrow<HashMap<String, String>>,
{
fn get(&self, key: &str) -> Result<String, std::env::VarError> {
self.borrow()
.get(key)
.ok_or(std::env::VarError::NotPresent)
.cloned()
}
}
impl<T> Write for T
where
T: std::borrow::BorrowMut<HashMap<String, String>>,
{
fn set(&mut self, key: String, value: String) {
self.borrow_mut().insert(key, value);
}
}
pub struct Std;
pub static ENV: Std = Std{};
impl Read for Std {
fn get(&self, key: &str) -> Result<String, std::env::VarError> {
std::env::var(key)
}
}
impl Write for Std {
fn set(&mut self, key: String, value: String) {
std::env::set_var(key, value);
}
}
pub trait Parse {
type Error: std::error::Error;
fn from_str(config: &str) -> Result<HashMap<String, String>, Self::Error>;
fn from_reader(reader: impl std::io::Read) -> Result<HashMap<String, String>, Self::Error>;
}
}
pub mod utils {
pub fn to_posix_path(path: impl AsRef<str>) -> String {
path.as_ref().replace('\\', "/")
}
pub fn to_win32_path(path: impl AsRef<str>) -> String {
path.as_ref().replace('/', "\\")
}
pub fn to_platform_path(path: impl AsRef<str>) -> String {
path.as_ref()
.replace(['/', '\\'], std::path::MAIN_SEPARATOR_STR)
}
pub fn escape_data(data: impl AsRef<str>) -> String {
data.as_ref()
.replace('%', "%25")
.replace('\r', "%0D")
.replace('\n', "%0A")
}
pub fn escape_property(prop: impl AsRef<str>) -> String {
prop.as_ref()
.replace('%', "%25")
.replace('\r', "%0D")
.replace('\n', "%0A")
.replace(':', "%3A")
.replace(',', "%2C")
}
}
pub mod summary {
pub const ENV_VAR: &str = "GITHUB_STEP_SUMMARY";
pub const DOCS_URL: &str = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct TableCell {
pub data: String,
pub header: bool,
pub colspan: usize,
pub rowspan: usize,
}
impl TableCell {
#[must_use]
pub fn new(data: String) -> Self {
Self {
data,
..Self::default()
}
}
#[must_use]
pub fn header(data: String) -> Self {
Self {
data,
header: true,
..Self::default()
}
}
}
impl Default for TableCell {
fn default() -> Self {
Self {
data: String::new(),
header: false,
colspan: 1,
rowspan: 1,
}
}
}
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone)]
pub struct ImageOptions {
width: Option<usize>,
height: Option<usize>,
}
}
pub fn prepare_kv_message(key: &str, value: &str) -> Result<String, ValueError> {
use uuid::Uuid;
let delimiter = format!("ghadelimiter_{}", Uuid::new_v4());
if key.contains(&delimiter) {
return Err(ValueError::ContainsDelimiter { delimiter });
}
if value.contains(&delimiter) {
return Err(ValueError::ContainsDelimiter { delimiter });
}
Ok(format!("{key}<<{delimiter}\n{value}\n{delimiter}"))
}
pub fn export_var(name: impl AsRef<str>, value: impl Into<String>) -> Result<(), CommandError> {
let value = value.into();
std::env::set_var(name.as_ref(), &value);
if std::env::var("GITHUB_ENV").and_then(not_empty).is_ok() {
let message = prepare_kv_message(name.as_ref(), &value)?;
issue_file_command("ENV", message)?;
return Ok(());
}
issue(
&CommandBuilder::new("set-env", value)
.property("name", name.as_ref())
.build(),
);
Ok(())
}
pub fn set_secret(secret: impl Into<String>) {
issue(&CommandBuilder::new("add-mask", secret).build());
}
fn prepend_to_path(path: impl AsRef<Path>) -> Result<(), std::env::JoinPathsError> {
if let Some(old_path) = std::env::var_os("PATH") {
let paths = [path.as_ref().to_path_buf()]
.into_iter()
.chain(std::env::split_paths(&old_path));
let new_path = std::env::join_paths(paths)?;
std::env::set_var("PATH", new_path);
}
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum AddPathError {
#[error(transparent)]
File(#[from] FileCommandError),
#[error(transparent)]
Join(#[from] std::env::JoinPathsError),
}
pub fn add_path(path: impl AsRef<Path>) -> Result<(), AddPathError> {
let path_string = path.as_ref().to_string_lossy();
prepend_to_path(path.as_ref())?;
if std::env::var("GITHUB_PATH").and_then(not_empty).is_ok() {
issue_file_command("PATH", &path_string)?;
} else {
issue(&CommandBuilder::new("add-path", path_string).build());
}
Ok(())
}
pub trait Parse {
type Input;
fn parse<E: env::Read>(env: &E) -> HashMap<Self::Input, Option<String>>;
}
pub trait ParseInput: Sized {
type Error: std::error::Error;
fn parse(value: String) -> Result<Self, Self::Error>;
}
#[derive(thiserror::Error, Debug, PartialEq, Eq, Hash, Clone)]
pub enum ParseError {
#[error("invalid boolean value \"{0}\"")]
Bool(String),
}
impl ParseInput for String {
type Error = std::convert::Infallible;
fn parse(value: String) -> Result<Self, Self::Error> {
Ok(value)
}
}
impl ParseInput for bool {
type Error = ParseError;
fn parse(value: String) -> Result<Self, Self::Error> {
match value.to_ascii_lowercase().as_str() {
"yes" | "true" | "t" => Ok(true),
"no" | "false" | "f" => Ok(false),
_ => Err(ParseError::Bool(value)),
}
}
}
pub fn get_input<T>(name: impl AsRef<str>) -> Result<Option<T>, <T as ParseInput>::Error>
where
T: ParseInput,
{
match get_raw_input(&env::ENV, name) {
Ok(input) => Some(T::parse(input)).transpose(),
Err(_) => Ok(None),
}
}
pub fn not_empty(value: String) -> Result<String, std::env::VarError> {
if value.is_empty() {
Err(std::env::VarError::NotPresent)
} else {
Ok(value)
}
}
pub fn get_raw_input(
env: &impl env::Read,
name: impl AsRef<str>,
) -> Result<String, std::env::VarError> {
env.get(&input_env_var(name.as_ref())).and_then(not_empty)
}
pub fn get_input_from<T>(
env: &impl env::Read,
name: impl AsRef<str>,
) -> Result<Option<T>, <T as ParseInput>::Error>
where
T: ParseInput,
{
match get_raw_input(env, name) {
Ok(input) => Some(T::parse(input)).transpose(),
Err(_) => Ok(None),
}
}
pub fn get_multiline_input(name: impl AsRef<str>) -> Result<Vec<String>, std::env::VarError> {
let value = get_raw_input(&env::ENV, name)?;
Ok(value.lines().map(ToOwned::to_owned).collect())
}
pub fn set_command_echo(enabled: bool) {
issue(&CommandBuilder::new("echo", if enabled { "on" } else { "off" }).build());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
pub enum ExitCode {
Success = 0,
Failure = 1,
}
pub fn fail(message: impl std::fmt::Display) {
error!("{}", message);
std::process::exit(ExitCode::Failure as i32);
}
#[must_use]
pub fn is_debug() -> bool {
std::env::var("RUNNER_DEBUG")
.map(|v| v.trim() == "1")
.unwrap_or(false)
}
#[derive(Debug)]
pub struct CommandBuilder {
command: String,
message: String,
props: HashMap<String, String>,
}
impl CommandBuilder {
#[must_use]
pub fn new(command: impl Into<String>, message: impl Into<String>) -> Self {
Self {
command: command.into(),
message: message.into(),
props: HashMap::new(),
}
}
#[must_use]
pub fn property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.props.insert(key.into(), value.into());
self
}
#[must_use]
pub fn properties(mut self, props: HashMap<String, String>) -> Self {
self.props.extend(props.into_iter());
self
}
#[must_use]
pub fn build(self) -> Command {
let Self {
command,
message,
props,
} = self;
Command {
command,
message,
props,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Command {
command: String,
message: String,
props: HashMap<String, String>,
}
impl Command {
#[must_use]
pub fn new(command: String, message: String, props: HashMap<String, String>) -> Self {
Self {
command,
message,
props,
}
}
}
impl std::fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const CMD_STRING: &str = "::";
write!(f, "{}{}", CMD_STRING, self.command)?;
if !self.props.is_empty() {
write!(f, " ")?;
}
for (i, (k, v)) in self.props.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
if v.is_empty() {
continue;
}
write!(f, "{k}={}", utils::escape_property(v))?;
}
write!(f, "{}{}", CMD_STRING, self.message)
}
}
pub fn issue(cmd: &Command) {
println!("{cmd}");
}
#[derive(thiserror::Error, Debug)]
pub enum ValueError {
#[error("should not contain delimiter `{delimiter}`")]
ContainsDelimiter { delimiter: String },
}
#[derive(thiserror::Error, Debug)]
pub enum FileCommandError {
#[error("missing env variable for file command {cmd}")]
Missing {
source: std::env::VarError,
cmd: String,
},
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Value(#[from] ValueError),
}
#[derive(thiserror::Error, Debug)]
pub enum CommandError {
#[error(transparent)]
File(#[from] FileCommandError),
#[error(transparent)]
Value(#[from] ValueError),
}
pub fn issue_file_command(
command: impl AsRef<str>,
message: impl AsRef<str>,
) -> Result<(), FileCommandError> {
use std::io::Write;
let key = format!("GITHUB_{}", command.as_ref());
let file_path = std::env::var(key).map_err(|source| FileCommandError::Missing {
source,
cmd: command.as_ref().to_string(),
})?;
let file = std::fs::OpenOptions::new()
.append(true)
.write(true)
.open(file_path)?;
let mut file = std::io::BufWriter::new(file);
writeln!(file, "{}", message.as_ref())?;
Ok(())
}
#[derive(Default, Debug, Hash, PartialEq, Eq)]
pub struct AnnotationProperties {
pub title: Option<String>,
pub file: Option<String>,
pub start_line: Option<usize>,
pub end_line: Option<usize>,
pub start_column: Option<usize>,
pub end_column: Option<usize>,
}
impl<H> From<AnnotationProperties> for HashMap<String, String, H>
where
H: std::hash::BuildHasher + Default,
{
fn from(props: AnnotationProperties) -> Self {
[
("title".to_string(), props.title),
("file".to_string(), props.file),
(
"line".to_string(),
props.start_line.map(|line| line.to_string()),
),
(
"endLine".to_string(),
props.end_line.map(|line| line.to_string()),
),
(
"col".to_string(),
props.start_column.map(|col| col.to_string()),
),
(
"endColumn".to_string(),
props.end_column.map(|col| col.to_string()),
),
]
.into_iter()
.filter_map(|(k, v)| v.map(|v| (k, v)))
.collect()
}
}
pub fn issue_level(
level: LogLevel,
message: impl Into<String>,
props: Option<AnnotationProperties>,
) {
let props = props.unwrap_or_default();
issue(
&CommandBuilder::new(level.to_string(), message)
.properties(props.into())
.build(),
);
}
#[macro_export]
macro_rules! debug {
($($arg:tt)*) => {{
$crate::issue_level(
$crate::LogLevel::Debug,
format!($($arg)*),
None,
);
}};
}
#[macro_export]
macro_rules! warning {
($($arg:tt)*) => {{
$crate::issue_level(
$crate::LogLevel::Warning,
format!($($arg)*),
None,
);
}};
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {{
$crate::issue_level(
$crate::LogLevel::Error,
format!($($arg)*),
None,
);
}};
}
#[macro_export]
macro_rules! notice {
($($arg:tt)*) => {{
$crate::issue_level(
$crate::LogLevel::Notice,
format!($($arg)*),
None,
);
}};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => { println!($($arg)*); };
}
pub fn start_group(name: impl Into<String>) {
issue(&CommandBuilder::new("group", name).build());
}
pub fn end_group() {
issue(&CommandBuilder::new("endgroup", "").build());
}
pub fn save_state(name: impl AsRef<str>, value: impl Into<String>) -> Result<(), CommandError> {
if std::env::var("GITHUB_STATE").and_then(not_empty).is_ok() {
let message = prepare_kv_message(name.as_ref(), &value.into())?;
issue_file_command("STATE", message)?;
return Ok(());
}
issue(
&CommandBuilder::new("save-state", value)
.property("name", name.as_ref())
.build(),
);
Ok(())
}
#[must_use]
pub fn get_state(name: impl AsRef<str>) -> Option<String> {
std::env::var(format!("STATE_{}", name.as_ref())).ok()
}
pub async fn group<T>(name: impl Into<String>, fut: impl std::future::Future<Output = T>) -> T {
start_group(name);
let res: T = fut.await;
end_group();
res
}
#[cfg(test)]
mod tests {
use super::env::Env;
#[test]
fn test_env() {
let input_name = "SOME_NAME";
let env = Env::from_iter([(input_name.to_string(), "SET".to_string())]);
dbg!(&env);
assert_eq!(env.get("INPUT_SOME_NAME"), Some(&"SET".to_string()));
}
#[test]
fn test_get_non_empty_input() {
let input_name = "SOME_NAME";
let env = Env::from_iter([(input_name.to_string(), "SET".to_string())]);
dbg!(&env);
assert_eq!(
super::get_input_from::<String>(&env, input_name),
Ok(Some("SET".to_string()))
);
}
#[test]
fn test_get_empty_input() {
let input_name = "SOME_NAME";
let mut env = Env::from_iter([]);
assert_eq!(super::get_input_from::<String>(&env, input_name), Ok(None),);
env.insert(input_name.to_string(), String::new());
assert_eq!(super::get_input_from::<String>(&env, input_name), Ok(None),);
}
}