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, Default, Clone, PartialEq, Serialize, Deserialize)]
19pub enum ActionMode {
20 #[default]
22 Default,
23 Container,
25 Installer,
27 Entrypoint,
29 CustomComposite,
31}
32
33#[derive(Debug, PartialEq, Serialize, Deserialize)]
37pub struct ActionYML {
38 #[serde(skip)]
40 pub mode: ActionMode,
41
42 #[serde(skip)]
44 pub path: Option<PathBuf>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub name: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub description: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub author: Option<String>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub branding: Option<ActionBranding>,
59
60 pub inputs: IndexMap<String, ActionInput>,
62 pub outputs: IndexMap<String, ActionOutput>,
64 #[serde(skip)]
66 pub output_value_step_id: Option<String>,
67
68 pub runs: ActionRuns,
70}
71
72impl Default for ActionYML {
73 fn default() -> Self {
74 ActionYML {
75 mode: ActionMode::Default,
76 path: None,
77 name: Some(env!("CARGO_PKG_NAME").to_string()),
78 description: None,
79 author: None,
80 branding: None,
81 inputs: IndexMap::new(),
82 outputs: IndexMap::new(),
83 output_value_step_id: Some("cargo-run".to_string()),
84 runs: ActionRuns::default(),
85 }
86 }
87}
88
89impl ActionYML {
90 pub fn set_container_image(&mut self, image: String) {
92 self.runs.using = ActionRunUsing::Docker;
93 self.runs.image = Some(image);
94 self.runs.steps = None;
95 self.output_value_step_id = None;
97 }
98
99 pub fn add_installer_step(&mut self) {
104 if self.runs.steps.is_none() {
105 self.runs.steps = Some(vec![]);
106 }
107
108 let binary_name =
109 std::env::var("CARGO_BIN_NAME").unwrap_or_else(|_| "${{ github.action }}".to_string());
110
111 let env = IndexMap::from([
112 (
113 "ACTION_REPOSITORY".to_string(),
114 "${{ github.action_repository }}".to_string(),
115 ),
116 (
117 "ACTION_REF".to_string(),
118 "${{ github.action_ref }}".to_string(),
119 ),
120 ("BINARY_NAME".to_string(), binary_name.to_string()),
121 ("GH_TOKEN".to_string(), "${{ github.token }}".to_string()),
122 ("RUNNER_OS".to_string(), "${{ runner.os }}".to_string()),
123 ("RUNNER_ARCH".to_string(), "${{ runner.arch }}".to_string()),
124 ]);
125
126 if let Some(ref mut steps) = self.runs.steps {
127 steps.push(ActionRunStep {
129 name: Some("Install the Action".to_string()),
130 id: Some("install-action".to_string()),
131 shell: Some("bash".to_string()),
132 condition: Some("${{ runner.os == 'Linux' || runner.os == 'macOS' }}".to_string()),
133 env: Some(env.clone()),
134 run: Some(include_str!("installer.sh").to_string()),
135 });
136 }
138 }
139
140 pub fn add_script(&mut self, script: &str, id: Option<&str>) {
142 if self.runs.steps.is_none() {
143 self.runs.steps = Some(vec![]);
144 }
145
146 if let Some(ref mut steps) = self.runs.steps {
147 steps.push(ActionRunStep {
149 id: id.map(|s| s.to_string()),
150 name: Some("Installing Action".to_string()),
151 shell: Some("bash".to_string()),
152 run: Some(script.to_string()),
153 ..Default::default()
154 });
155 }
156 }
157
158 pub fn add_script_run(&mut self) {
160 if self.runs.steps.is_none() {
161 self.runs.steps = Some(vec![]);
162 }
163
164 if let Some(ref mut steps) = self.runs.steps {
165 self.output_value_step_id = Some("cargo-run".to_string());
166
167 let binary_name = std::env::var("CARGO_BIN_NAME")
168 .unwrap_or_else(|_| "${{ github.action }}".to_string());
169 let script = format!("set -e\n{}", binary_name);
170 steps.push(ActionRunStep {
172 name: Some("Run the Action".to_string()),
173 id: Some("cargo-run".to_string()),
174 shell: Some("bash".to_string()),
175 run: Some(script.to_string()),
176 ..Default::default()
177 });
178 }
179 }
180
181 pub fn add_cargo_install_step(&mut self, binary_name: &str) {
183 if self.runs.steps.is_none() {
184 self.runs.steps = Some(vec![]);
185 }
186
187 if let Some(ref mut steps) = self.runs.steps {
188 let cmd = if binary_name == "." {
189 "cargo install --path .".to_string()
190 } else {
191 format!("cargo install \"{binary_name}\"")
192 };
193
194 steps.push(ActionRunStep {
195 name: Some("Cargo Install".to_string()),
196 shell: Some("bash".to_string()),
197 run: Some(cmd),
198 ..Default::default()
199 });
200 }
201 }
202
203 pub fn load_action(path: String) -> Result<ActionYML, Box<dyn std::error::Error>> {
205 let fhandle = std::fs::File::open(&path)?;
206 let mut action_yml: ActionYML = serde_yaml::from_reader(fhandle)?;
207 action_yml.path = Some(PathBuf::from(path.clone()));
208 Ok(action_yml)
209 }
210
211 pub fn write(&self) -> Result<PathBuf, ActionsError> {
213 if let Some(ref path) = self.path {
214 if !path.exists() {
215 let parent = path.parent().unwrap();
216 std::fs::create_dir_all(parent)
217 .map_err(|err| ActionsError::IOError(err.to_string()))?;
218 }
219
220 let mut content = String::new();
221 content.push_str("# This file is generated by ghactions\n");
222 if self.mode == ActionMode::CustomComposite {
223 content.push_str(
224 "# `ghactions` is generating all parts but not composite action steps\n",
225 );
226 } else {
227 content.push_str(
228 "# Do not edit this file manually unless you disable the `generate` feature.\n\n",
229 );
230 }
231 content.push_str(
232 serde_yaml::to_string(self)
233 .map_err(|err| ActionsError::IOError(err.to_string()))?
234 .as_str(),
235 );
236
237 let mut fhandle = std::fs::OpenOptions::new()
239 .write(true)
240 .create(true)
241 .truncate(true)
242 .open(path)
243 .map_err(|err| ActionsError::IOError(err.to_string()))?;
244 fhandle
245 .write_all(content.as_bytes())
246 .map_err(|err| ActionsError::IOError(err.to_string()))?;
247
248 Ok(path.clone())
249 } else {
250 Err(ActionsError::NotImplemented)
251 }
252 }
253}
254
255#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
257pub struct ActionInput {
258 #[serde(skip)]
260 pub action_name: String,
261 #[serde(skip)]
263 pub field_name: String,
264 #[serde(skip)]
266 pub r#type: String,
267
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub description: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub required: Option<bool>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub default: Option<String>,
277 #[serde(rename = "deprecationMessage", skip_serializing_if = "Option::is_none")]
279 pub deprecation_message: Option<String>,
280
281 #[serde(skip)]
284 pub separator: Option<String>,
285}
286
287#[derive(Debug, PartialEq, Default, Serialize, Deserialize)]
289pub struct ActionOutput {
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub description: Option<String>,
293
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub value: Option<String>,
297}
298
299#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
303pub struct ActionBranding {
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub color: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub icon: Option<String>,
310}
311
312#[derive(Debug, PartialEq, Serialize, Deserialize)]
314pub struct ActionRuns {
315 pub using: ActionRunUsing,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub image: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub args: Option<Vec<String>>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub steps: Option<Vec<ActionRunStep>>,
328}
329
330impl Default for ActionRuns {
331 fn default() -> Self {
332 Self {
333 using: ActionRunUsing::Composite,
334 image: None,
335 args: None,
336 steps: None,
337 }
338 }
339}
340
341fn default_composite_steps() -> Vec<ActionRunStep> {
342 let binary_name = std::env::var("CARGO_BIN_NAME").unwrap_or_else(|_| "action".to_string());
344 vec![
345 ActionRunStep {
354 name: Some("Compile / Install the Action binary".to_string()),
355 shell: Some("bash".to_string()),
356 run: Some("set -e\ncargo install --path \"${{ github.action_path }}\"".to_string()),
357 ..Default::default()
358 },
359 ActionRunStep {
361 id: Some("cargo-run".to_string()),
362 name: Some("Run the Action".to_string()),
363 shell: Some("bash".to_string()),
364 run: Some(format!("set -e\n{}", binary_name)),
365 ..Default::default()
366 },
367 ]
368}
369
370#[derive(Debug, PartialEq, Deserialize)]
372pub enum ActionRunUsing {
373 Docker,
375 Composite,
377}
378
379impl From<&str> for ActionRunUsing {
380 fn from(value: &str) -> Self {
381 match value {
382 "docker" => ActionRunUsing::Docker,
383 "composite" => ActionRunUsing::Composite,
384 _ => ActionRunUsing::Composite,
385 }
386 }
387}
388
389impl From<String> for ActionRunUsing {
390 fn from(value: String) -> Self {
391 Self::from(value.as_str())
392 }
393}
394
395impl Serialize for ActionRunUsing {
396 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
397 where
398 S: Serializer,
399 {
400 match self {
401 ActionRunUsing::Docker => serializer.serialize_str("docker"),
402 ActionRunUsing::Composite => serializer.serialize_str("composite"),
403 }
404 }
405}
406
407#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
409pub struct ActionRunStep {
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub name: Option<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub id: Option<String>,
416 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
418 pub condition: Option<String>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub env: Option<IndexMap<String, String>>,
423
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub shell: Option<String>,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub run: Option<String>,
430}