github_actions_models/workflow/
job.rs1use indexmap::IndexMap;
4use serde::{de, Deserialize};
5use serde_yaml::Value;
6
7use crate::common::expr::{BoE, LoE};
8use crate::common::{Env, If, Permissions, Uses};
9
10use super::{Concurrency, Defaults};
11
12#[derive(Deserialize)]
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(Debug, Deserialize, 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 if group.is_none() && labels.is_empty() {
68 return Err(de::Error::custom(
69 "runs-on must provide either `group` or one or more `labels`",
70 ));
71 }
72 }
73
74 Ok(runs_on)
75 }
76}
77
78#[derive(Deserialize)]
79#[serde(rename_all = "kebab-case", untagged)]
80pub enum DeploymentEnvironment {
81 Name(String),
82 NameURL { name: String, url: Option<String> },
83}
84
85#[derive(Deserialize)]
86#[serde(rename_all = "kebab-case")]
87pub struct Step {
88 pub id: Option<String>,
90
91 pub r#if: Option<If>,
93
94 pub name: Option<String>,
96
97 pub timeout_minutes: Option<LoE<u64>>,
99
100 #[serde(default)]
103 pub continue_on_error: BoE,
104
105 #[serde(flatten)]
107 pub body: StepBody,
108}
109
110#[derive(Deserialize)]
111#[serde(rename_all = "kebab-case", untagged)]
112pub enum StepBody {
113 Uses {
114 #[serde(deserialize_with = "crate::common::step_uses")]
116 uses: Uses,
117
118 #[serde(default)]
120 with: Env,
121 },
122 Run {
123 #[serde(deserialize_with = "crate::common::bool_is_string")]
125 run: String,
126
127 working_directory: Option<String>,
129
130 shell: Option<String>,
133
134 #[serde(default)]
136 env: LoE<Env>,
137 },
138}
139
140#[derive(Deserialize)]
141#[serde(rename_all = "kebab-case")]
142pub struct Strategy {
143 pub matrix: Option<LoE<Matrix>>,
144 pub fail_fast: Option<BoE>,
145 pub max_parallel: Option<LoE<u64>>,
146}
147
148#[derive(Deserialize)]
149#[serde(rename_all = "kebab-case")]
150pub struct Matrix {
151 #[serde(default)]
152 pub include: LoE<Vec<IndexMap<String, Value>>>,
153 #[serde(default)]
154 pub exclude: LoE<Vec<IndexMap<String, Value>>>,
155 #[serde(flatten)]
156 pub dimensions: LoE<IndexMap<String, LoE<Vec<Value>>>>,
157}
158
159#[derive(Deserialize)]
160#[serde(rename_all = "kebab-case", untagged)]
161pub enum Container {
162 Name(String),
163 Container {
164 image: String,
165 credentials: Option<DockerCredentials>,
166 #[serde(default)]
167 env: LoE<Env>,
168 #[serde(default)]
170 volumes: Vec<String>,
171 options: Option<String>,
172 },
173}
174
175#[derive(Deserialize)]
176pub struct DockerCredentials {
177 pub username: Option<String>,
178 pub password: Option<String>,
179}
180
181#[derive(Deserialize)]
182#[serde(rename_all = "kebab-case")]
183pub struct ReusableWorkflowCallJob {
184 pub name: Option<String>,
185 #[serde(default)]
186 pub permissions: Permissions,
187 #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
188 pub needs: Vec<String>,
189 pub r#if: Option<If>,
190 #[serde(deserialize_with = "crate::common::reusable_step_uses")]
191 pub uses: Uses,
192 #[serde(default)]
193 pub with: Env,
194 pub secrets: Option<Secrets>,
195}
196
197#[derive(Deserialize, Debug, PartialEq)]
198#[serde(rename_all = "kebab-case")]
199pub enum Secrets {
200 Inherit,
201 #[serde(untagged)]
202 Env(#[serde(default)] Env),
203}
204
205#[cfg(test)]
206mod tests {
207 use crate::{
208 common::{expr::LoE, EnvValue},
209 workflow::job::{Matrix, Secrets},
210 };
211
212 use super::{RunsOn, Strategy};
213
214 #[test]
215 fn test_secrets() {
216 assert_eq!(
217 serde_yaml::from_str::<Secrets>("inherit").unwrap(),
218 Secrets::Inherit
219 );
220
221 let secrets = "foo-secret: bar";
222 let Secrets::Env(secrets) = serde_yaml::from_str::<Secrets>(secrets).unwrap() else {
223 panic!("unexpected secrets variant");
224 };
225 assert_eq!(secrets["foo-secret"], EnvValue::String("bar".into()));
226 }
227
228 #[test]
229 fn test_strategy_matrix_expressions() {
230 let strategy = "matrix: ${{ 'foo' }}";
231 let Strategy {
232 matrix: Some(LoE::Expr(expr)),
233 ..
234 } = serde_yaml::from_str::<Strategy>(strategy).unwrap()
235 else {
236 panic!("unexpected matrix variant");
237 };
238
239 assert_eq!(expr.as_curly(), "${{ 'foo' }}");
240
241 let strategy = r#"
242matrix:
243 foo: ${{ 'foo' }}
244"#;
245
246 let Strategy {
247 matrix:
248 Some(LoE::Literal(Matrix {
249 include: _,
250 exclude: _,
251 dimensions: LoE::Literal(dims),
252 })),
253 ..
254 } = serde_yaml::from_str::<Strategy>(strategy).unwrap()
255 else {
256 panic!("unexpected matrix variant");
257 };
258
259 assert!(matches!(dims.get("foo"), Some(LoE::Expr(_))));
260 }
261
262 #[test]
263 fn test_runson_invalid_state() {
264 let runson = "group: \nlabels: []";
265
266 assert_eq!(
267 serde_yaml::from_str::<RunsOn>(runson)
268 .unwrap_err()
269 .to_string(),
270 "runs-on must provide either `group` or one or more `labels`"
271 );
272 }
273}