ghactions_core/actions/
models.rs

1//! # Models
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize, Serializer};
5use std::{
6    collections::HashMap,
7    fmt::{Display, Formatter},
8    io::Write,
9    os::unix::fs::FileExt,
10    path::PathBuf,
11};
12
13use crate::ActionsError;
14
15const GHACTIONS_ROOT: &str = env!("CARGO_MANIFEST_DIR");
16
17/// Action YAML file structure
18///
19/// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
20#[derive(Debug, PartialEq, Serialize, Deserialize)]
21pub struct ActionYML {
22    /// Action Path
23    #[serde(skip)]
24    pub path: Option<PathBuf>,
25
26    /// Action Name
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub name: Option<String>,
29    /// Action Description
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub description: Option<String>,
32    /// Action Author
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub author: Option<String>,
35
36    /// Action Branding
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub branding: Option<ActionBranding>,
39
40    /// Action Inputs
41    pub inputs: IndexMap<String, ActionInput>,
42    /// Action Outputs
43    pub outputs: IndexMap<String, ActionOutput>,
44    /// Output Value Step ID
45    #[serde(skip)]
46    pub output_value_step_id: Option<String>,
47
48    /// Action Runs
49    pub runs: ActionRuns,
50}
51
52impl Default for ActionYML {
53    fn default() -> Self {
54        ActionYML {
55            path: None,
56            name: Some(env!("CARGO_PKG_NAME").to_string()),
57            description: None,
58            author: None,
59            branding: None,
60            inputs: IndexMap::new(),
61            outputs: IndexMap::new(),
62            output_value_step_id: Some("cargo-run".to_string()),
63            runs: ActionRuns::default(),
64        }
65    }
66}
67
68impl ActionYML {
69    /// Set the Action to a Container Image based Action
70    pub fn set_container_image(&mut self, image: PathBuf) {
71        self.runs.using = ActionRunUsing::Docker;
72        self.runs.image = Some(image);
73        self.runs.steps = None;
74        // Docker based action doesn't need to set the output value step id
75        self.output_value_step_id = None;
76    }
77
78    /// Load the Action YAML file
79    pub fn load_action(path: String) -> Result<ActionYML, Box<dyn std::error::Error>> {
80        let fhandle = std::fs::File::open(&path)?;
81        let mut action_yml: ActionYML = serde_yaml::from_reader(fhandle)?;
82        action_yml.path = Some(PathBuf::from(path.clone()));
83        Ok(action_yml)
84    }
85
86    /// Write the Action YAML file
87    pub fn write(&self) -> Result<PathBuf, ActionsError> {
88        if let Some(ref path) = self.path {
89            if !path.exists() {
90                let parent = path.parent().unwrap();
91                std::fs::create_dir_all(parent)
92                    .map_err(|err| ActionsError::IOError(err.to_string()))?;
93            }
94
95            let mut content = String::new();
96            content.push_str("# This file is generated by ghactions\n");
97            content.push_str(
98                "# Do not edit this file manually unless you disable the `generate` feature.\n\n",
99            );
100            content.push_str(
101                serde_yaml::to_string(self)
102                    .map_err(|err| ActionsError::IOError(err.to_string()))?
103                    .as_str(),
104            );
105
106            // Create or Open the file
107            let mut fhandle = std::fs::OpenOptions::new()
108                .write(true)
109                .create(true)
110                .truncate(true)
111                .open(path)
112                .map_err(|err| ActionsError::IOError(err.to_string()))?;
113            fhandle
114                .write_all(content.as_bytes())
115                .map_err(|err| ActionsError::IOError(err.to_string()))?;
116
117            Ok(path.clone())
118        } else {
119            Err(ActionsError::NotImplemented)
120        }
121    }
122}
123
124/// Action Input structure
125#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
126pub struct ActionInput {
127    /// [internal] Action Field Name
128    #[serde(skip)]
129    pub action_name: String,
130    /// [internal] Struct Field Name
131    #[serde(skip)]
132    pub field_name: String,
133    /// [internal] Input Type
134    #[serde(skip)]
135    pub r#type: String,
136
137    /// Input Description
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub description: Option<String>,
140    /// Input Required or not
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub required: Option<bool>,
143    /// Input Default value
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub default: Option<String>,
146    /// Deprecation Message
147    #[serde(rename = "deprecationMessage", skip_serializing_if = "Option::is_none")]
148    pub deprecation_message: Option<String>,
149
150    // Other internal fields
151    /// Separator
152    #[serde(skip)]
153    pub separator: Option<String>,
154}
155
156/// Action Output structure
157#[derive(Debug, PartialEq, Default, Serialize, Deserialize)]
158pub struct ActionOutput {
159    /// Output Description
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub description: Option<String>,
162
163    /// Output Value
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub value: Option<String>,
166}
167
168/// Action Branding
169///
170/// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#branding
171#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
172pub struct ActionBranding {
173    /// Color
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub color: Option<String>,
176    /// Icon
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub icon: Option<String>,
179}
180
181/// Action Runs structure
182#[derive(Debug, PartialEq, Serialize, Deserialize)]
183pub struct ActionRuns {
184    /// Action Name
185    pub using: ActionRunUsing,
186
187    /// Container Image (container actions only)
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub image: Option<PathBuf>,
190    /// Arguments (container actions only)
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub args: Option<Vec<String>>,
193
194    /// Steps (composite actions only)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub steps: Option<Vec<ActionRunStep>>,
197}
198
199impl Default for ActionRuns {
200    fn default() -> Self {
201        Self {
202            using: ActionRunUsing::Composite,
203            image: None,
204            args: None,
205            steps: Some(default_composite_steps()),
206        }
207    }
208}
209
210fn default_composite_steps() -> Vec<ActionRunStep> {
211    // Binary Name
212    let binary_name = std::env::var("CARGO_BIN_NAME").unwrap_or_else(|_| "action".to_string());
213    vec![
214        // Step 1 - Checking for Cargo/Rust (needs to be installed by the user)
215        // ActionRunStep {
216        //     name: Some("Checking for Cargo/Rust".to_string()),
217        //     shell: Some("bash".to_string()),
218        //     run: Some("".to_string()),
219        //     ..Default::default()
220        // },
221        // Step 2 - Compile the Action
222        ActionRunStep {
223            name: Some("Compile / Install the Action binary".to_string()),
224            shell: Some("bash".to_string()),
225            run: Some("set -e\ncargo install --path \"${{ github.action_path }}\"".to_string()),
226            ..Default::default()
227        },
228        // Step 3 - Run the Action
229        ActionRunStep {
230            id: Some("cargo-run".to_string()),
231            name: Some("Run the Action".to_string()),
232            shell: Some("bash".to_string()),
233            run: Some(format!("set -e\n{}", binary_name)),
234            ..Default::default()
235        },
236    ]
237}
238
239/// Action Run Using Enum
240#[derive(Debug, PartialEq, Deserialize)]
241pub enum ActionRunUsing {
242    /// Docker / Container Image
243    Docker,
244    /// Composite Action
245    Composite,
246}
247
248impl From<&str> for ActionRunUsing {
249    fn from(value: &str) -> Self {
250        match value {
251            "docker" => ActionRunUsing::Docker,
252            "composite" => ActionRunUsing::Composite,
253            _ => ActionRunUsing::Composite,
254        }
255    }
256}
257
258impl From<String> for ActionRunUsing {
259    fn from(value: String) -> Self {
260        Self::from(value.as_str())
261    }
262}
263
264impl Serialize for ActionRunUsing {
265    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
266    where
267        S: Serializer,
268    {
269        match self {
270            ActionRunUsing::Docker => serializer.serialize_str("docker"),
271            ActionRunUsing::Composite => serializer.serialize_str("composite"),
272        }
273    }
274}
275
276/// Action Run Step
277#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
278pub struct ActionRunStep {
279    /// Step ID
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub id: Option<String>,
282    /// Step Name
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub name: Option<String>,
285    /// Shell to use (if any)
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub shell: Option<String>,
288    /// Run command
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub run: Option<String>,
291
292    /// Environment Variables
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub env: Option<HashMap<String, String>>,
295}