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::{DockerUses, 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<LoE<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 #[serde(deserialize_with = "crate::common::docker_uses")]
166 image: DockerUses,
167 credentials: Option<DockerCredentials>,
168 #[serde(default)]
169 env: LoE<Env>,
170 #[serde(default)]
172 volumes: Vec<String>,
173 options: Option<String>,
174 },
175}
176
177#[derive(Deserialize, Debug)]
178pub struct DockerCredentials {
179 pub username: Option<String>,
180 pub password: Option<String>,
181}
182
183#[derive(Deserialize, Debug)]
184#[serde(rename_all = "kebab-case")]
185pub struct ReusableWorkflowCallJob {
186 pub name: Option<String>,
187 #[serde(default)]
188 pub permissions: Permissions,
189 #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
190 pub needs: Vec<String>,
191 pub r#if: Option<If>,
192 #[serde(deserialize_with = "crate::common::reusable_step_uses")]
193 pub uses: Uses,
194 #[serde(default)]
195 pub with: Env,
196 pub secrets: Option<Secrets>,
197}
198
199#[derive(Deserialize, Debug, PartialEq)]
200#[serde(rename_all = "kebab-case")]
201pub enum Secrets {
202 Inherit,
203 #[serde(untagged)]
204 Env(#[serde(default)] Env),
205}
206
207#[cfg(test)]
208mod tests {
209 use crate::{
210 common::{EnvValue, expr::LoE},
211 workflow::job::{Matrix, Secrets},
212 };
213
214 use super::{RunsOn, Strategy};
215
216 #[test]
217 fn test_secrets() {
218 assert_eq!(
219 serde_yaml::from_str::<Secrets>("inherit").unwrap(),
220 Secrets::Inherit
221 );
222
223 let secrets = "foo-secret: bar";
224 let Secrets::Env(secrets) = serde_yaml::from_str::<Secrets>(secrets).unwrap() else {
225 panic!("unexpected secrets variant");
226 };
227 assert_eq!(secrets["foo-secret"], EnvValue::String("bar".into()));
228 }
229
230 #[test]
231 fn test_strategy_matrix_expressions() {
232 let strategy = "matrix: ${{ 'foo' }}";
233 let Strategy {
234 matrix: Some(LoE::Expr(expr)),
235 ..
236 } = serde_yaml::from_str::<Strategy>(strategy).unwrap()
237 else {
238 panic!("unexpected matrix variant");
239 };
240
241 assert_eq!(expr.as_curly(), "${{ 'foo' }}");
242
243 let strategy = r#"
244matrix:
245 foo: ${{ 'foo' }}
246"#;
247
248 let Strategy {
249 matrix:
250 Some(LoE::Literal(Matrix {
251 include: _,
252 exclude: _,
253 dimensions: LoE::Literal(dims),
254 })),
255 ..
256 } = serde_yaml::from_str::<Strategy>(strategy).unwrap()
257 else {
258 panic!("unexpected matrix variant");
259 };
260
261 assert!(matches!(dims.get("foo"), Some(LoE::Expr(_))));
262 }
263
264 #[test]
265 fn test_runson_invalid_state() {
266 let runson = "group: \nlabels: []";
267
268 assert_eq!(
269 serde_yaml::from_str::<RunsOn>(runson)
270 .unwrap_err()
271 .to_string(),
272 "runs-on must provide either `group` or one or more `labels`"
273 );
274 }
275}