gh_workflow/
step.rs

1//!
2//! Step-related structures and implementations for GitHub workflow steps.
3
4use derive_setters::Setters;
5use indexmap::IndexMap;
6use merge::Merge;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::toolchain::{Abi, Arch, Component, System, Target, Toolchain, Vendor, Version};
11use crate::{private, Artifacts, Env, Expression, RetryStrategy};
12
13/// Represents a step in the workflow.
14#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
15#[serde(transparent)]
16pub struct Step<A> {
17    /// The value of the step.
18    pub value: StepValue,
19    #[serde(skip)]
20    pub marker: A,
21}
22
23impl From<Step<Run>> for StepValue {
24    /// Converts a `Step<Run>` into a `StepValue`.
25    fn from(step: Step<Run>) -> Self {
26        step.value
27    }
28}
29
30impl From<Step<Use>> for StepValue {
31    /// Converts a `Step<Use>` into a `StepValue`.
32    fn from(step: Step<Use>) -> Self {
33        step.value
34    }
35}
36
37/// Represents a step that uses an action.
38#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
39pub struct Use;
40
41/// Represents a step that runs a command.
42#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
43pub struct Run;
44
45/// A trait to convert `Step<Run>` and `Step<Use>` to `StepValue`.
46pub trait StepType: Sized + private::Sealed {
47    /// Converts a step to its value representation.
48    fn to_value(s: Step<Self>) -> StepValue;
49}
50
51impl private::Sealed for Run {}
52impl private::Sealed for Use {}
53
54impl StepType for Run {
55    /// Converts a `Step<Run>` to `StepValue`.
56    fn to_value(s: Step<Self>) -> StepValue {
57        s.into()
58    }
59}
60
61impl StepType for Use {
62    /// Converts a `Step<Use>` to `StepValue`.
63    fn to_value(s: Step<Self>) -> StepValue {
64        s.into()
65    }
66}
67
68/// Represents input parameters for a step.
69#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
70#[serde(transparent)]
71pub struct Input(#[serde(skip_serializing_if = "IndexMap::is_empty")] pub IndexMap<String, Value>);
72
73impl From<IndexMap<String, Value>> for Input {
74    /// Converts an `IndexMap` into an `Input`.
75    fn from(value: IndexMap<String, Value>) -> Self {
76        Input(value)
77    }
78}
79
80impl Merge for Input {
81    /// Merges another `Input` into this one.
82    fn merge(&mut self, other: Self) {
83        self.0.extend(other.0);
84    }
85}
86
87impl Input {
88    /// Adds a new input parameter to the `Input`.
89    pub fn add<S: ToString, V: Into<Value>>(mut self, key: S, value: V) -> Self {
90        self.0.insert(key.to_string(), value.into());
91        self
92    }
93
94    /// Checks if the `Input` is empty.
95    pub fn is_empty(&self) -> bool {
96        self.0.is_empty()
97    }
98}
99
100/// Represents a step value in the workflow.
101#[allow(clippy::duplicated_attributes)]
102#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Merge)]
103#[serde(rename_all = "kebab-case")]
104#[setters(
105    strip_option,
106    into,
107    generate_delegates(ty = "Step<Run>", field = "value"),
108    generate_delegates(ty = "Step<Use>", field = "value")
109)]
110pub struct StepValue {
111    /// The ID of the step.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub id: Option<String>,
114
115    /// The name of the step.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub name: Option<String>,
118
119    /// The condition under which the step runs.
120    #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
121    pub if_condition: Option<Expression>,
122
123    /// The action to use in the step.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    #[setters(skip)]
126    pub uses: Option<String>,
127
128    /// Input parameters for the step.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub with: Option<Input>,
131
132    /// The command to run in the step.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    #[setters(skip)]
135    pub run: Option<String>,
136
137    /// Environment variables for the step.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub env: Option<Env>,
140
141    /// The timeout for the step in minutes.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub timeout_minutes: Option<u32>,
144
145    /// Whether to continue on error.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub continue_on_error: Option<bool>,
148
149    /// The working directory for the step.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub working_directory: Option<String>,
152
153    /// The retry strategy for the step.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub retry: Option<RetryStrategy>,
156
157    /// Artifacts produced by the step.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub artifacts: Option<Artifacts>,
160}
161
162impl StepValue {
163    /// Creates a new `StepValue` that runs the provided shell command.
164    pub fn run<T: ToString>(cmd: T) -> Self {
165        StepValue { run: Some(cmd.to_string()), ..Default::default() }
166    }
167
168    /// Creates a new `StepValue` that uses an action.
169    pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
170        owner: Owner,
171        repo: Repo,
172        version: Version,
173    ) -> Self {
174        StepValue {
175            uses: Some(format!(
176                "{}/{}@{}",
177                owner.to_string(),
178                repo.to_string(),
179                version.to_string()
180            )),
181            ..Default::default()
182        }
183    }
184}
185
186/// Represents a step in the workflow.
187impl<T> Step<T> {
188    /// Adds an environment variable to the step.
189    pub fn add_env<R: Into<Env>>(mut self, new_env: R) -> Self {
190        let mut env = self.value.env.take().unwrap_or_default();
191
192        env.0.extend(new_env.into().0);
193        self.value.env = Some(env);
194        self
195    }
196}
197
198impl Step<()> {
199    pub fn new(name: impl ToString) -> Self {
200        Step {
201            value: StepValue::default().name(name.to_string()),
202            marker: Default::default(),
203        }
204    }
205
206    pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
207        mut self,
208        owner: Owner,
209        repo: Repo,
210        version: Version,
211    ) -> Step<Use> {
212        self.value.merge(StepValue::uses(owner, repo, version));
213        Step { value: self.value, marker: Default::default() }
214    }
215
216    pub fn run(mut self, cmd: impl ToString) -> Step<Run> {
217        self.value.merge(StepValue::run(cmd));
218        Step { value: self.value, marker: Default::default() }
219    }
220}
221
222/// Represents a step that uses an action.
223impl Step<Use> {
224    /// Creates a step pointing to the default GitHub's Checkout Action.
225    pub fn checkout() -> Step<Use> {
226        Step::new("Checkout Code").uses("actions", "checkout", "v5")
227    }
228
229    /// Adds a new input to the step.
230    pub fn add_with<I: Into<Input>>(mut self, new_with: I) -> Self {
231        let mut with = self.value.with.take().unwrap_or_default();
232        with.merge(new_with.into());
233        if with.0.is_empty() {
234            self.value.with = None;
235        } else {
236            self.value.with = Some(with);
237        }
238
239        self
240    }
241}
242
243/// Represents a key-value pair for inputs.
244impl<S1: ToString, S2: ToString> From<(S1, S2)> for Input {
245    /// Converts a tuple into an `Input`.
246    fn from(value: (S1, S2)) -> Self {
247        let mut index_map: IndexMap<String, Value> = IndexMap::new();
248        index_map.insert(value.0.to_string(), Value::String(value.1.to_string()));
249        Input(index_map)
250    }
251}
252
253impl Step<Toolchain> {
254    pub fn toolchain() -> Step<Toolchain> {
255        Step { value: Default::default(), marker: Toolchain::default() }
256    }
257
258    pub fn add_version(mut self, version: Version) -> Self {
259        self.marker.version.push(version);
260        self
261    }
262
263    pub fn add_component(mut self, component: Component) -> Self {
264        self.marker.components.push(component);
265        self
266    }
267
268    pub fn add_stable(mut self) -> Self {
269        self.marker.version.push(Version::Stable);
270        self
271    }
272
273    pub fn add_nightly(mut self) -> Self {
274        self.marker.version.push(Version::Nightly);
275        self
276    }
277
278    pub fn add_clippy(mut self) -> Self {
279        self.marker.components.push(Component::Clippy);
280        self
281    }
282
283    pub fn add_fmt(mut self) -> Self {
284        self.marker.components.push(Component::Rustfmt);
285        self
286    }
287
288    pub fn target(mut self, arch: Arch, vendor: Vendor, system: System, abi: Option<Abi>) -> Self {
289        self.marker.target = Some(Target { arch, vendor, system, abi });
290        self
291    }
292}
293
294impl StepType for Toolchain {
295    fn to_value(s: Step<Self>) -> StepValue {
296        let step: Step<Use> = s.marker.into();
297        StepValue::from(step)
298    }
299}