ghactions_core/
lib.rs

1//! ghactions-core is a library that provides core functionality for GitHub Actions in Rust.
2#![allow(dead_code, unused_imports)]
3#![deny(missing_docs)]
4
5#[cfg(feature = "log")]
6extern crate log;
7
8use std::{
9    io::Write,
10    path::{Path, PathBuf},
11};
12
13pub mod actions;
14pub mod errors;
15#[cfg(feature = "log")]
16pub mod logging;
17pub mod repository;
18
19pub use crate::actions::models::{ActionInput, ActionRuns, ActionYML};
20pub use crate::errors::ActionsError;
21pub use crate::repository::reference::RepositoryReference;
22
23/// Action Trait
24pub trait ActionTrait {
25    /// Parse the action input
26    fn init() -> Result<Self, ActionsError>
27    where
28        Self: Sized;
29
30    /// Get the action name
31    fn name(&self) -> &str;
32
33    /// Get the action description
34    fn description(&self) -> &str;
35
36    /// Get the input value for a provided key
37    fn get_input(key: impl Into<String> + Copy) -> Result<String, ActionsError> {
38        let key = key.into();
39        std::env::var(&key).map_err(|_| ActionsError::InputError(key))
40    }
41
42    /// Get the input value for a provided key as a boolean
43    fn get_input_bool(key: impl Into<String> + Copy) -> Result<bool, ActionsError> {
44        Self::get_input(key)?
45            .parse::<bool>()
46            .map_err(|_| ActionsError::InputTypeError(key.into(), "bool".into()))
47    }
48
49    /// Get the input value for a provided key as an integer
50    fn get_input_int(key: impl Into<String> + Copy) -> Result<i32, ActionsError> {
51        Self::get_input(key)?
52            .parse::<i32>()
53            .map_err(|_| ActionsError::InputTypeError(key.into(), "int".into()))
54    }
55
56    /// Get the input value for a provided key as a vector using a seperator
57    fn get_input_vec(
58        key: impl Into<String> + Copy,
59        seperator: &str,
60    ) -> Result<Vec<String>, ActionsError> {
61        Ok(Self::get_input(key)?
62            .split(seperator)
63            .map(|s| s.to_string())
64            .collect::<Vec<String>>())
65    }
66
67    /// Set the output value for a provided key
68    fn set_output(key: impl Into<String>, value: impl Into<String>) -> Result<(), ActionsError> {
69        let key = key.into();
70        let value = value.into();
71
72        let output_file = Self::get_output_path();
73        let output_path = PathBuf::from(output_file.clone());
74
75        if !output_path.exists() {
76            #[cfg(feature = "log")]
77            log::debug!("Creating output file: {}", output_path.display());
78            std::fs::File::create(&output_path)?;
79        }
80
81        match std::fs::OpenOptions::new().append(true).open(output_file) {
82            Ok(mut file) => {
83                writeln!(file, "{key}={value}")?;
84            }
85            Err(e) => {
86                #[cfg(feature = "log")]
87                log::error!("Failed to open output file: {e}");
88
89                // If we can't open the file, print to stdout
90                println!("::set-output name={key}::{value}");
91            }
92        }
93
94        Ok(())
95    }
96
97    /// Get the Octocrab instance
98    ///
99    /// Uses the `GITHUB_API_URL` and `GITHUB_TOKEN` environment variable to create an Octocrab instance
100    #[cfg(feature = "octocrab")]
101    fn octocrab(&self) -> Result<octocrab::Octocrab, ActionsError> {
102        #[cfg(feature = "log")]
103        {
104            log::debug!("Creating Octocrab instance");
105            log::debug!("URL: {}", self.get_api_url());
106        }
107
108        match self.get_token() {
109            Ok(token) => self.octocrab_with_token(token),
110            Err(_) => {
111                #[cfg(feature = "log")]
112                log::warn!("No GitHub Token provided");
113
114                self.octocrab_without_token()
115            }
116        }
117    }
118
119    /// Get the Octocrab instance with a specific token
120    #[cfg(feature = "octocrab")]
121    fn octocrab_with_token(
122        &self,
123        token: impl Into<String>,
124    ) -> Result<octocrab::Octocrab, ActionsError> {
125        let token = token.into();
126        #[cfg(feature = "log")]
127        log::debug!("Creating Octocrab instance with token");
128
129        if token.is_empty() {
130            return Err(ActionsError::OctocrabError(
131                "Token cannot be empty".to_string(),
132            ));
133        }
134
135        octocrab::Octocrab::builder()
136            .base_uri(self.get_api_url())
137            .map_err(|e| ActionsError::OctocrabError(e.to_string()))?
138            .add_header(
139                http::header::ACCEPT,
140                "application/vnd.github.v3+json".to_string(),
141            )
142            .personal_token(token)
143            .build()
144            .map_err(|e| ActionsError::OctocrabError(e.to_string()))
145    }
146
147    /// Get the Octocrab instance without a token
148    #[cfg(feature = "octocrab")]
149    fn octocrab_without_token(&self) -> Result<octocrab::Octocrab, ActionsError> {
150        #[cfg(feature = "log")]
151        log::debug!("Creating Octocrab instance without token");
152
153        octocrab::Octocrab::builder()
154            .base_uri(self.get_api_url())
155            .map_err(|e| ActionsError::OctocrabError(e.to_string()))?
156            .add_header(
157                http::header::ACCEPT,
158                "application/vnd.github.v3+json".to_string(),
159            )
160            .build()
161            .map_err(|e| ActionsError::OctocrabError(e.to_string()))
162    }
163
164    /// Get the GitHub Actions Output File
165    ///
166    /// https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
167    fn get_output_path() -> String {
168        if let Ok(ghout) = std::env::var("GITHUB_OUTPUT") {
169            #[cfg(feature = "log")]
170            log::debug!("GITHUB_OUTPUT: {ghout}");
171            ghout
172        } else if let Ok(ghout) = std::env::var("GITHUB_STATE") {
173            #[cfg(feature = "log")]
174            log::debug!("GITHUB_STATE: {ghout}");
175            ghout
176        } else {
177            #[cfg(feature = "log")]
178            log::debug!("Default Output: /tmp/github_actions.env");
179            "/tmp/github_actions.env".to_string()
180        }
181    }
182
183    /// GetHub Server URL (default: https://github.com)
184    fn get_server_url(&self) -> String {
185        Self::get_input("GITHUB_SERVER_URL").unwrap_or_else(|_| "https://github.com".into())
186    }
187    /// GitHub API URL (default: https://api.github.com)
188    fn get_api_url(&self) -> String {
189        Self::get_input("GITHUB_API_URL").unwrap_or_else(|_| "https://api.github.com".into())
190    }
191    /// GitHub GraphQL URL (default: https://api.github.com/graphql)
192    fn get_graphql_url(&self) -> String {
193        Self::get_input("GITHUB_GRAPHQL_URL")
194            .unwrap_or_else(|_| "https://api.github.com/graphql".into())
195    }
196
197    /// Get the GitHub Token
198    ///
199    /// Checks both the `GITHUB_TOKEN` and `ACTIONS_RUNTIME_TOKEN` environment variables
200    fn get_token(&self) -> Result<String, ActionsError> {
201        Self::get_input("GITHUB_TOKEN").or_else(|_| Self::get_input("ACTIONS_RUNTIME_TOKEN"))
202    }
203    /// Get the GitHub SHA
204    fn get_sha(&self) -> Result<String, ActionsError> {
205        Self::get_input("GITHUB_SHA")
206    }
207    /// Get the GitHub Ref (full)
208    fn get_ref(&self) -> Result<String, ActionsError> {
209        Self::get_input("GITHUB_REF")
210    }
211    /// Get the GitHub Ref Type
212    fn get_ref_type(&self) -> Result<String, ActionsError> {
213        Self::get_input("GITHUB_REF_TYPE")
214    }
215    /// Get the GitHub Ref Name
216    fn get_ref_name(&self) -> Result<String, ActionsError> {
217        Self::get_input("GITHUB_REF_NAME")
218    }
219
220    /// Get the GitHub Workflow Event Name
221    fn get_event_name(&self) -> Result<String, ActionsError> {
222        Self::get_input("GITHUB_EVENT_NAME")
223    }
224
225    /// Get the full GitHub Repository (owner/repo)
226    fn get_repository(&self) -> Result<String, ActionsError> {
227        Self::get_input("GITHUB_REPOSITORY")
228    }
229    /// Get the GitHub Repository owner name (org/user)
230    fn get_repository_owner(&self) -> Result<String, ActionsError> {
231        Self::get_input("GITHUB_REPOSITORY_OWNER").or_else(|_| {
232            self.get_repository()
233                .map(|r| r.split('/').collect::<Vec<&str>>()[0].to_string())
234        })
235    }
236    /// Get the GitHub Repository name
237    fn get_repository_name(&self) -> Result<String, ActionsError> {
238        self.get_repository()
239            .map(|r| r.split('/').collect::<Vec<&str>>()[1].to_string())
240    }
241    /// Get the GitHub Repository URL
242    fn get_repository_url(&self) -> Result<String, ActionsError> {
243        Self::get_input("GITHUB_REPOSITORYURL")
244    }
245    /// Get the Action Triggering Author
246    fn get_actor(&self) -> Result<String, ActionsError> {
247        Self::get_input("GITHUB_ACTOR")
248    }
249}