github_actions_models/workflow/
job.rs1use indexmap::IndexMap;
4use serde::Deserialize;
5use serde_yaml::Value;
6
7use crate::common::expr::{BoE, LoE};
8use crate::common::{Env, If, Permissions, Uses, custom_error};
9
10use super::{Concurrency, Defaults};
11
12#[derive(Deserialize, Debug)]
15#[serde(rename_all = "kebab-case")]
16pub struct NormalJob {
17 pub name: Option<String>,
18 #[serde(default)]
19 pub permissions: Permissions,
20 #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
21 pub needs: Vec<String>,
22 pub r#if: Option<If>,
23 pub runs_on: LoE<RunsOn>,
24 pub environment: Option<DeploymentEnvironment>,
25 pub concurrency: Option<Concurrency>,
26 #[serde(default)]
27 pub outputs: IndexMap<String, String>,
28 #[serde(default)]
29 pub env: LoE<Env>,
30 pub defaults: Option<Defaults>,
31 pub steps: Vec<Step>,
32 pub timeout_minutes: Option<LoE<u64>>,
33 pub strategy: Option<Strategy>,
34 #[serde(default)]
35 pub continue_on_error: BoE,
36 pub container: Option<Container>,
37 #[serde(default)]
38 pub services: IndexMap<String, Container>,
39}
40
41#[derive(Deserialize, Debug, PartialEq)]
42#[serde(rename_all = "kebab-case", untagged, remote = "Self")]
43pub enum RunsOn {
44 #[serde(deserialize_with = "crate::common::scalar_or_vector")]
45 Target(Vec<String>),
46 Group {
47 group: Option<String>,
48 #[serde(deserialize_with = "crate::common::scalar_or_vector", default)]
52 labels: Vec<String>,
53 },
54}
55
56impl<'de> Deserialize<'de> for RunsOn {
57 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58 where
59 D: serde::Deserializer<'de>,
60 {
61 let runs_on = Self::deserialize(deserializer)?;
62
63 if let RunsOn::Group { group, labels } = &runs_on
67 && group.is_none()
68 && labels.is_empty()
69 {
70 return Err(custom_error::<D>(
71 "runs-on must provide either `group` or one or more `labels`",
72 ));
73 }
74
75 Ok(runs_on)
76 }
77}
78
79#[derive(Deserialize, Debug)]
80#[serde(rename_all = "kebab-case", untagged)]
81pub enum DeploymentEnvironment {
82 Name(String),
83 NameURL { name: String, url: Option<String> },
84}
85
86#[derive(Deserialize, Debug)]
87#[serde(rename_all = "kebab-case")]
88pub struct Step {
89 pub id: Option<String>,
91
92 pub r#if: Option<If>,
94
95 pub name: Option<String>,
97
98 pub timeout_minutes: Option<LoE<u64>>,
100
101 #[serde(default)]
104 pub continue_on_error: BoE,
105
106 #[serde(default)]
108 pub env: LoE<Env>,
109
110 #[serde(flatten)]
112 pub body: StepBody,
113}
114
115#[derive(Deserialize, Debug)]
116#[serde(rename_all = "kebab-case", untagged)]
117pub enum StepBody {
118 Uses {
119 #[serde(deserialize_with = "crate::common::step_uses")]
121 uses: Uses,
122
123 #[serde(default)]
125 with: Env,
126 },
127 Run {
128 #[serde(deserialize_with = "crate::common::bool_is_string")]
130 run: String,
131
132 working_directory: Option<String>,
134
135 shell: Option<String>,
138 },
139}
140
141#[derive(Deserialize, Debug)]
142#[serde(rename_all = "kebab-case")]
143pub struct Strategy {
144 pub matrix: Option<LoE<Matrix>>,
145 pub fail_fast: Option<BoE>,
146 pub max_parallel: Option<LoE<u64>>,
147}
148
149#[derive(Deserialize, Debug)]
150#[serde(rename_all = "kebab-case")]
151pub struct Matrix {
152 #[serde(default)]
153 pub include: LoE<Vec<IndexMap<String, Value>>>,
154 #[serde(default)]
155 pub exclude: LoE<Vec<IndexMap<String, Value>>>,
156 #[serde(flatten)]
157 pub dimensions: LoE<IndexMap<String, LoE<Vec<Value>>>>,
158}
159
160#[derive(Deserialize, Debug)]
161#[serde(rename_all = "kebab-case", untagged)]
162pub enum Container {
163 Name(String),
164 Container {
165 image: String,
166 credentials: Option<DockerCredentials>,
167 #[serde(default)]
168 env: LoE<Env>,
169 #[serde(default)]
171 volumes: Vec<String>,
172 options: Option<String>,
173 },
174}
175
176#[derive(Deserialize, Debug)]
177pub struct DockerCredentials {
178 pub username: Option<String>,
179 pub password: Option<String>,
180}
181
182#[derive(Deserialize, Debug)]
183#[serde(rename_all = "kebab-case")]
184pub struct ReusableWorkflowCallJob {
185 pub name: Option<String>,
186 #[serde(default)]
187 pub permissions: Permissions,
188 #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
189 pub needs: Vec<String>,
190 pub r#if: Option<If>,
191 #[serde(deserialize_with = "crate::common::reusable_step_uses")]
192 pub uses: Uses,
193 #[serde(default)]
194 pub with: Env,
195 pub secrets: Option<Secrets>,
196}
197
198#[derive(Deserialize, Debug, PartialEq)]
199#[serde(rename_all = "kebab-case")]
200pub enum Secrets {
201 Inherit,
202 #[serde(untagged)]
203 Env(#[serde(default)] Env),
204}
205
206#[cfg(test)]
207mod tests {
208 use crate::{
209 common::{EnvValue, expr::LoE},
210 workflow::job::{Matrix, Secrets},
211 };
212
213 use super::{RunsOn, Strategy};
214
215 #[test]
216 fn test_secrets() {
217 assert_eq!(
218 serde_yaml::from_str::<Secrets>("inherit").unwrap(),
219 Secrets::Inherit
220 );
221
222 let secrets = "foo-secret: bar";
223 let Secrets::Env(secrets) = serde_yaml::from_str::<Secrets>(secrets).unwrap() else {
224 panic!("unexpected secrets variant");
225 };
226 assert_eq!(secrets["foo-secret"], EnvValue::String("bar".into()));
227 }
228
229 #[test]
230 fn test_strategy_matrix_expressions() {
231 let strategy = "matrix: ${{ 'foo' }}";
232 let Strategy {
233 matrix: Some(LoE::Expr(expr)),
234 ..
235 } = serde_yaml::from_str::<Strategy>(strategy).unwrap()
236 else {
237 panic!("unexpected matrix variant");
238 };
239
240 assert_eq!(expr.as_curly(), "${{ 'foo' }}");
241
242 let strategy = r#"
243matrix:
244 foo: ${{ 'foo' }}
245"#;
246
247 let Strategy {
248 matrix:
249 Some(LoE::Literal(Matrix {
250 include: _,
251 exclude: _,
252 dimensions: LoE::Literal(dims),
253 })),
254 ..
255 } = serde_yaml::from_str::<Strategy>(strategy).unwrap()
256 else {
257 panic!("unexpected matrix variant");
258 };
259
260 assert!(matches!(dims.get("foo"), Some(LoE::Expr(_))));
261 }
262
263 #[test]
264 fn test_runson_invalid_state() {
265 let runson = "group: \nlabels: []";
266
267 assert_eq!(
268 serde_yaml::from_str::<RunsOn>(runson)
269 .unwrap_err()
270 .to_string(),
271 "runs-on must provide either `group` or one or more `labels`"
272 );
273 }
274}