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