1use 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#[derive(Debug, PartialEq, Serialize, Deserialize)]
21pub struct ActionYML {
22 #[serde(skip)]
24 pub path: Option<PathBuf>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub name: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub description: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub author: Option<String>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub branding: Option<ActionBranding>,
39
40 pub inputs: IndexMap<String, ActionInput>,
42 pub outputs: IndexMap<String, ActionOutput>,
44 #[serde(skip)]
46 pub output_value_step_id: Option<String>,
47
48 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 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 self.output_value_step_id = None;
76 }
77
78 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 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 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#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
126pub struct ActionInput {
127 #[serde(skip)]
129 pub action_name: String,
130 #[serde(skip)]
132 pub field_name: String,
133 #[serde(skip)]
135 pub r#type: String,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub description: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub required: Option<bool>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub default: Option<String>,
146 #[serde(rename = "deprecationMessage", skip_serializing_if = "Option::is_none")]
148 pub deprecation_message: Option<String>,
149
150 #[serde(skip)]
153 pub separator: Option<String>,
154}
155
156#[derive(Debug, PartialEq, Default, Serialize, Deserialize)]
158pub struct ActionOutput {
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub description: Option<String>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub value: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
172pub struct ActionBranding {
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub color: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub icon: Option<String>,
179}
180
181#[derive(Debug, PartialEq, Serialize, Deserialize)]
183pub struct ActionRuns {
184 pub using: ActionRunUsing,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub image: Option<PathBuf>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub args: Option<Vec<String>>,
193
194 #[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 let binary_name = std::env::var("CARGO_BIN_NAME").unwrap_or_else(|_| "action".to_string());
213 vec![
214 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 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#[derive(Debug, PartialEq, Deserialize)]
241pub enum ActionRunUsing {
242 Docker,
244 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#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
278pub struct ActionRunStep {
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub id: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub name: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub shell: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub run: Option<String>,
291
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub env: Option<HashMap<String, String>>,
295}