gh_workflow/
job.rs

1//!
2//! Job-related structures and implementations for GitHub workflow jobs.
3
4use derive_setters::Setters;
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::concurrency::Concurrency;
10use crate::step::{Step, StepType, StepValue};
11use crate::{
12    Artifacts, Container, Defaults, Env, Expression, Permissions, RetryStrategy, Secret, Strategy,
13};
14
15/// Represents the environment in which a job runs.
16#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
17#[serde(transparent)]
18pub struct RunsOn(Value);
19
20impl<T> From<T> for RunsOn
21where
22    T: Into<Value>,
23{
24    /// Converts a value into a `RunsOn` instance.
25    fn from(value: T) -> Self {
26        Self(value.into())
27    }
28}
29
30/// Represents a job in the workflow.
31/// Field order matches GitHub Actions YAML structure for better readability.
32#[derive(Debug, Setters, Serialize, Deserialize, Clone, PartialEq, Eq)]
33#[serde(rename_all = "kebab-case")]
34#[setters(strip_option, into)]
35pub struct Job {
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub needs: Option<Vec<String>>,
38    #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
39    pub cond: Option<Expression>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub name: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub runs_on: Option<RunsOn>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub permissions: Option<Permissions>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub environment: Option<crate::Environment>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub concurrency: Option<Concurrency>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub outputs: Option<IndexMap<String, String>>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub env: Option<Env>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub defaults: Option<Defaults>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub timeout_minutes: Option<u32>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub continue_on_error: Option<bool>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub container: Option<Container>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub services: Option<IndexMap<String, Container>>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub strategy: Option<Strategy>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub steps: Option<Vec<StepValue>>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub uses: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub secrets: Option<IndexMap<String, Secret>>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub retry: Option<RetryStrategy>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub artifacts: Option<Artifacts>,
76}
77
78impl Default for Job {
79    /// Creates a default `Job` with `runs_on` set to "ubuntu-latest".
80    fn default() -> Self {
81        Self {
82            needs: None,
83            cond: None,
84            name: None,
85            runs_on: Some(RunsOn(Value::from("ubuntu-latest"))),
86            permissions: None,
87            environment: None,
88            concurrency: None,
89            outputs: None,
90            env: None,
91            defaults: None,
92            timeout_minutes: None,
93            continue_on_error: None,
94            container: None,
95            services: None,
96            strategy: None,
97            steps: None,
98            uses: None,
99            secrets: None,
100            retry: None,
101            artifacts: None,
102        }
103    }
104}
105
106impl Job {
107    /// Creates a new `Job` with the specified name and default settings.
108    pub fn new<T: ToString>(name: T) -> Self {
109        Self {
110            name: Some(name.to_string()),
111            runs_on: Some(RunsOn(Value::from("ubuntu-latest"))),
112            ..Default::default()
113        }
114    }
115
116    /// Adds a step to the job.
117    pub fn add_step<S: Into<Step<T>>, T: StepType>(mut self, step: S) -> Self {
118        let mut steps = self.steps.take().unwrap_or_default();
119        let step: Step<T> = step.into();
120        let step: StepValue = T::to_value(step);
121        steps.push(step);
122        self.steps = Some(steps);
123        self
124    }
125
126    /// Adds an environment variable to the job.
127    pub fn add_env<T: Into<Env>>(mut self, new_env: T) -> Self {
128        let mut env = self.env.take().unwrap_or_default();
129
130        env.0.extend(new_env.into().0);
131        self.env = Some(env);
132        self
133    }
134
135    pub fn add_needs<J: ToString>(mut self, job_id: J) -> Self {
136        if let Some(needs) = self.needs.as_mut() {
137            needs.push(job_id.to_string());
138        } else {
139            self.needs = Some(vec![job_id.to_string()]);
140        }
141        self
142    }
143
144    /// Adds an output to the job.
145    pub fn add_output<K: ToString, V: ToString>(mut self, key: K, value: V) -> Self {
146        let mut outputs = self.outputs.take().unwrap_or_default();
147        outputs.insert(key.to_string(), value.to_string());
148        self.outputs = Some(outputs);
149        self
150    }
151
152    /// Adds a service to the job.
153    pub fn add_service<K: ToString, V: Into<Container>>(mut self, key: K, service: V) -> Self {
154        let mut services = self.services.take().unwrap_or_default();
155        services.insert(key.to_string(), service.into());
156        self.services = Some(services);
157        self
158    }
159
160    /// Adds a secret to the job.
161    pub fn add_secret<K: ToString, V: Into<Secret>>(mut self, key: K, secret: V) -> Self {
162        let mut secrets = self.secrets.take().unwrap_or_default();
163        secrets.insert(key.to_string(), secret.into());
164        self.secrets = Some(secrets);
165        self
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_job_default_sets_runs_on() {
175        let job = Job::default();
176        assert!(job.runs_on.is_some());
177
178        // Verify it's set to "ubuntu-latest"
179        if let Some(runs_on) = job.runs_on {
180            assert_eq!(
181                runs_on.0,
182                serde_json::Value::String("ubuntu-latest".to_string())
183            );
184        }
185    }
186}