1use std::fmt::Display;
5
6use derive_setters::Setters;
7use indexmap::IndexMap;
8use merge::Merge;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::error::Result;
13use crate::generate::Generate;
14use crate::{private, Event};
15
16#[derive(Debug, Default, Serialize, Deserialize, Clone)]
17#[serde(transparent)]
18pub struct Jobs(pub(crate) IndexMap<String, Job>);
19impl Jobs {
20 pub fn insert(&mut self, key: String, value: Job) {
21 self.0.insert(key, value);
22 }
23
24 pub fn get(&self, key: &str) -> Option<&Job> {
34 self.0.get(key)
35 }
36}
37
38#[derive(Debug, Default, Setters, Serialize, Deserialize, Clone)]
45#[serde(rename_all = "kebab-case")]
46#[setters(strip_option, into)]
47pub struct Workflow {
48 #[serde(skip_serializing_if = "Option::is_none")]
51 pub name: Option<String>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub env: Option<Env>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub run_name: Option<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
65 pub on: Option<Event>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub permissions: Option<Permissions>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub jobs: Option<Jobs>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
78 pub concurrency: Option<Concurrency>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub defaults: Option<Defaults>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub secrets: Option<IndexMap<String, Secret>>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub timeout_minutes: Option<u32>,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
95#[serde(rename_all = "kebab-case")]
96pub struct EventAction {
97 #[serde(skip_serializing_if = "Vec::is_empty")]
99 branches: Vec<String>,
100
101 #[serde(skip_serializing_if = "Vec::is_empty")]
103 branches_ignore: Vec<String>,
104}
105
106impl Workflow {
107 pub fn new<T: ToString>(name: T) -> Self {
109 Self { name: Some(name.to_string()), ..Default::default() }
110 }
111
112 pub fn to_string(&self) -> Result<String> {
114 Ok(serde_yaml::to_string(self)?)
115 }
116
117 pub fn add_job_when<T: ToString, J: Into<Job>>(self, cond: bool, id: T, job: J) -> Self {
119 if cond {
120 self.add_job(id, job)
121 } else {
122 self
123 }
124 }
125
126 pub fn add_job<T: ToString, J: Into<Job>>(mut self, id: T, job: J) -> Self {
128 let key = id.to_string();
129 let mut jobs = self.jobs.unwrap_or_default();
130
131 jobs.insert(key, job.into());
132
133 self.jobs = Some(jobs);
134 self
135 }
136
137 pub fn parse(yml: &str) -> Result<Self> {
139 Ok(serde_yaml::from_str(yml)?)
140 }
141
142 pub fn generate(self) -> Result<()> {
144 Generate::new(self).generate()
145 }
146
147 pub fn add_event<T: Into<Event>>(mut self, that: T) -> Self {
149 if let Some(mut this) = self.on.take() {
150 this.merge(that.into());
151 self.on = Some(this);
152 } else {
153 self.on = Some(that.into());
154 }
155 self
156 }
157
158 pub fn add_event_when<T: Into<Event>>(self, cond: bool, that: T) -> Self {
160 if cond {
161 self.add_event(that)
162 } else {
163 self
164 }
165 }
166
167 pub fn add_env<T: Into<Env>>(mut self, new_env: T) -> Self {
169 let mut env = self.env.unwrap_or_default();
170
171 env.0.extend(new_env.into().0);
172 self.env = Some(env);
173 self
174 }
175
176 pub fn add_env_when<T: Into<Env>>(self, cond: bool, new_env: T) -> Self {
178 if cond {
179 self.add_env(new_env)
180 } else {
181 self
182 }
183 }
184
185 pub fn get_id(&self, job: &Job) -> Option<&str> {
187 self.jobs
188 .as_ref()?
189 .0
190 .iter()
191 .find(|(_, j)| *j == job)
192 .map(|(id, _)| id.as_str())
193 }
194}
195
196#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
198#[serde(rename_all = "kebab-case")]
199pub enum ActivityType {
200 Created,
201 Edited,
202 Deleted,
203}
204
205#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
207#[serde(transparent)]
208pub struct RunsOn(Value);
209
210impl<T> From<T> for RunsOn
211where
212 T: Into<Value>,
213{
214 fn from(value: T) -> Self {
216 RunsOn(value.into())
217 }
218}
219
220#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
222#[serde(rename_all = "kebab-case")]
223#[setters(strip_option, into)]
224pub struct Job {
225 #[serde(skip_serializing_if = "Option::is_none")]
226 #[setters(skip)]
227 pub(crate) needs: Option<Vec<String>>,
228
229 #[serde(skip)]
230 pub(crate) tmp_needs: Option<Vec<Job>>,
231
232 #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
233 pub cond: Option<Expression>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub name: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub runs_on: Option<RunsOn>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub permissions: Option<Permissions>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub env: Option<Env>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub strategy: Option<Strategy>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub steps: Option<Vec<StepValue>>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub uses: Option<String>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub container: Option<Container>,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub outputs: Option<IndexMap<String, String>>,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub concurrency: Option<Concurrency>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub timeout_minutes: Option<u32>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub services: Option<IndexMap<String, Container>>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub secrets: Option<IndexMap<String, Secret>>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub defaults: Option<Defaults>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub continue_on_error: Option<bool>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub retry: Option<RetryStrategy>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub artifacts: Option<Artifacts>,
268}
269
270impl Job {
271 pub fn new<T: ToString>(name: T) -> Self {
273 Self {
274 name: Some(name.to_string()),
275 runs_on: Some(RunsOn(Value::from("ubuntu-latest"))),
276 ..Default::default()
277 }
278 }
279
280 pub fn add_step_when<S: Into<Step<T>>, T: StepType>(self, cond: bool, step: S) -> Self {
282 if cond {
283 self.add_step(step)
284 } else {
285 self
286 }
287 }
288
289 pub fn add_step<S: Into<Step<T>>, T: StepType>(mut self, step: S) -> Self {
291 let mut steps = self.steps.unwrap_or_default();
292 let step: Step<T> = step.into();
293 let step: StepValue = T::to_value(step);
294 steps.push(step);
295 self.steps = Some(steps);
296 self
297 }
298
299 pub fn add_env<T: Into<Env>>(mut self, new_env: T) -> Self {
301 let mut env = self.env.unwrap_or_default();
302
303 env.0.extend(new_env.into().0);
304 self.env = Some(env);
305 self
306 }
307
308 pub fn add_env_when<T: Into<Env>>(self, cond: bool, new_env: T) -> Self {
310 if cond {
311 self.add_env(new_env)
312 } else {
313 self
314 }
315 }
316
317 pub fn add_steps<T: StepType, I: IntoIterator<Item = Step<T>>>(mut self, steps: I) -> Self {
322 for step in steps {
323 self = self.add_step(step);
324 }
325 self
326 }
327
328 pub fn add_needs<J: Into<Job>>(mut self, needs: J) -> Self {
329 let job: Job = needs.into();
330 let mut needs = self.tmp_needs.unwrap_or_default();
331 needs.push(job);
332 self.tmp_needs = Some(needs);
333 self
334 }
335
336 pub fn add_needs_when<T: Into<Job>>(self, cond: bool, needs: T) -> Self {
338 if cond {
339 self.add_needs(needs)
340 } else {
341 self
342 }
343 }
344}
345
346#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
348#[serde(transparent)]
349pub struct Step<A> {
350 value: StepValue,
352 #[serde(skip)]
353 marker: std::marker::PhantomData<A>,
354}
355
356impl From<Step<Run>> for StepValue {
357 fn from(step: Step<Run>) -> Self {
359 step.value
360 }
361}
362
363impl From<Step<Use>> for StepValue {
364 fn from(step: Step<Use>) -> Self {
366 step.value
367 }
368}
369
370#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
372pub struct Use;
373
374#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
376pub struct Run;
377
378pub trait StepType: Sized + private::Sealed {
380 fn to_value(s: Step<Self>) -> StepValue;
382}
383
384impl private::Sealed for Run {}
385impl private::Sealed for Use {}
386
387impl StepType for Run {
388 fn to_value(s: Step<Self>) -> StepValue {
390 s.into()
391 }
392}
393
394impl StepType for Use {
395 fn to_value(s: Step<Self>) -> StepValue {
397 s.into()
398 }
399}
400
401#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
403#[serde(transparent)]
404pub struct Env(IndexMap<String, Value>);
405
406impl From<IndexMap<String, Value>> for Env {
407 fn from(value: IndexMap<String, Value>) -> Self {
409 Env(value)
410 }
411}
412
413impl Env {
414 pub fn github() -> Self {
416 let mut map = IndexMap::new();
417 map.insert(
418 "GITHUB_TOKEN".to_string(),
419 Value::from("${{ secrets.GITHUB_TOKEN }}"),
420 );
421 Env(map)
422 }
423
424 pub fn new<K: ToString, V: Into<Value>>(key: K, value: V) -> Self {
426 Env::default().add(key, value)
427 }
428
429 pub fn add<T1: ToString, T2: Into<Value>>(mut self, key: T1, value: T2) -> Self {
431 self.0.insert(key.to_string(), value.into());
432 self
433 }
434}
435
436#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
438#[serde(transparent)]
439pub struct Input(#[serde(skip_serializing_if = "IndexMap::is_empty")] IndexMap<String, Value>);
440
441impl From<IndexMap<String, Value>> for Input {
442 fn from(value: IndexMap<String, Value>) -> Self {
444 Input(value)
445 }
446}
447
448impl Merge for Input {
449 fn merge(&mut self, other: Self) {
451 self.0.extend(other.0);
452 }
453}
454
455impl Input {
456 pub fn add<S: ToString, V: Into<Value>>(mut self, key: S, value: V) -> Self {
458 self.0.insert(key.to_string(), value.into());
459 self
460 }
461
462 pub fn is_empty(&self) -> bool {
464 self.0.is_empty()
465 }
466}
467
468#[allow(clippy::duplicated_attributes)]
470#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
471#[serde(rename_all = "kebab-case")]
472#[setters(
473 strip_option,
474 into,
475 generate_delegates(ty = "Step<Run>", field = "value"),
476 generate_delegates(ty = "Step<Use>", field = "value")
477)]
478pub struct StepValue {
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub id: Option<String>,
482
483 #[serde(skip_serializing_if = "Option::is_none")]
485 pub name: Option<String>,
486
487 #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
489 pub if_condition: Option<Expression>,
490
491 #[serde(skip_serializing_if = "Option::is_none")]
493 #[setters(skip)]
494 pub uses: Option<String>,
495
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub with: Option<Input>,
499
500 #[serde(skip_serializing_if = "Option::is_none")]
502 #[setters(skip)]
503 pub run: Option<String>,
504
505 #[serde(skip_serializing_if = "Option::is_none")]
507 pub env: Option<Env>,
508
509 #[serde(skip_serializing_if = "Option::is_none")]
511 pub timeout_minutes: Option<u32>,
512
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub continue_on_error: Option<bool>,
516
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub working_directory: Option<String>,
520
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub retry: Option<RetryStrategy>,
524
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub artifacts: Option<Artifacts>,
528}
529
530impl StepValue {
531 pub fn run<T: ToString>(cmd: T) -> Self {
533 StepValue { run: Some(cmd.to_string()), ..Default::default() }
534 }
535
536 pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
538 owner: Owner,
539 repo: Repo,
540 version: Version,
541 ) -> Self {
542 StepValue {
543 uses: Some(format!(
544 "{}/{}@{}",
545 owner.to_string(),
546 repo.to_string(),
547 version.to_string()
548 )),
549 ..Default::default()
550 }
551 }
552}
553
554impl<T> Step<T> {
556 pub fn add_env<R: Into<Env>>(mut self, new_env: R) -> Self {
558 let mut env = self.value.env.unwrap_or_default();
559
560 env.0.extend(new_env.into().0);
561 self.value.env = Some(env);
562 self
563 }
564}
565
566impl Step<Run> {
568 pub fn run<T: ToString>(cmd: T) -> Self {
570 Step { value: StepValue::run(cmd), marker: Default::default() }
571 }
572}
573
574impl Step<Use> {
576 pub fn uses<Owner: ToString, Repo: ToString, Version: ToString>(
578 owner: Owner,
579 repo: Repo,
580 version: Version,
581 ) -> Self {
582 Step {
583 value: StepValue::uses(owner, repo, version),
584 marker: Default::default(),
585 }
586 }
587
588 pub fn checkout() -> Step<Use> {
590 Step::uses("actions", "checkout", "v4").name("Checkout Code")
591 }
592
593 pub fn add_with<I: Into<Input>>(mut self, new_with: I) -> Self {
595 let mut with = self.value.with.unwrap_or_default();
596 with.merge(new_with.into());
597 if with.0.is_empty() {
598 self.value.with = None;
599 } else {
600 self.value.with = Some(with);
601 }
602
603 self
604 }
605
606 pub fn add_with_when<I: Into<Input>>(self, cond: bool, new_with: I) -> Self {
608 if cond {
609 self.add_with(new_with)
610 } else {
611 self
612 }
613 }
614}
615
616impl<S1: ToString, S2: ToString> From<(S1, S2)> for Input {
618 fn from(value: (S1, S2)) -> Self {
620 let mut index_map: IndexMap<String, Value> = IndexMap::new();
621 index_map.insert(value.0.to_string(), Value::String(value.1.to_string()));
622 Input(index_map)
623 }
624}
625
626impl<S1: Display, S2: Display> From<(S1, S2)> for Env {
628 fn from(value: (S1, S2)) -> Self {
630 let mut index_map: IndexMap<String, Value> = IndexMap::new();
631 index_map.insert(value.0.to_string(), Value::String(value.1.to_string()));
632 Env(index_map)
633 }
634}
635
636#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
638#[serde(rename_all = "kebab-case")]
639pub enum Runner {
640 #[default]
641 Linux,
642 MacOS,
643 Windows,
644 Custom(String),
645}
646
647#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
649#[serde(rename_all = "kebab-case")]
650#[setters(strip_option, into)]
651pub struct Container {
652 pub image: String,
654
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub credentials: Option<Credentials>,
658
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub env: Option<Env>,
662
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub ports: Option<Vec<Port>>,
666
667 #[serde(skip_serializing_if = "Option::is_none")]
669 pub volumes: Option<Vec<Volume>>,
670
671 #[serde(skip_serializing_if = "Option::is_none")]
673 pub options: Option<String>,
674
675 #[serde(skip_serializing_if = "Option::is_none")]
677 pub hostname: Option<String>,
678}
679
680#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
682#[serde(rename_all = "kebab-case")]
683#[setters(strip_option, into)]
684pub struct Credentials {
685 pub username: String,
687
688 pub password: String,
690}
691
692#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
694#[serde(rename_all = "kebab-case")]
695pub enum Port {
696 Number(u16),
698
699 Name(String),
701}
702
703#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
705#[serde(rename_all = "kebab-case")]
706#[setters(strip_option, into)]
707pub struct Volume {
708 pub source: String,
710
711 pub destination: String,
713}
714
715impl Volume {
716 pub fn new(volume_str: &str) -> Option<Self> {
718 let parts: Vec<&str> = volume_str.split(':').collect();
719 if parts.len() == 2 {
720 Some(Volume {
721 source: parts[0].to_string(),
722 destination: parts[1].to_string(),
723 })
724 } else {
725 None
726 }
727 }
728}
729
730#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
732#[serde(rename_all = "kebab-case")]
733#[setters(strip_option, into)]
734pub struct Concurrency {
735 pub group: String,
737
738 #[serde(skip_serializing_if = "Option::is_none")]
740 pub cancel_in_progress: Option<bool>,
741
742 #[serde(skip_serializing_if = "Option::is_none")]
744 pub limit: Option<u32>,
745}
746
747impl Concurrency {
748 pub fn new(group: impl Into<Expression>) -> Self {
749 let expr: Expression = group.into();
750 Self { group: expr.0, ..Default::default() }
751 }
752}
753
754#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
756#[serde(rename_all = "kebab-case")]
757#[setters(strip_option, into)]
758pub struct Permissions {
759 #[serde(skip_serializing_if = "Option::is_none")]
761 pub actions: Option<Level>,
762
763 #[serde(skip_serializing_if = "Option::is_none")]
765 pub contents: Option<Level>,
766
767 #[serde(skip_serializing_if = "Option::is_none")]
769 pub issues: Option<Level>,
770
771 #[serde(skip_serializing_if = "Option::is_none")]
773 pub pull_requests: Option<Level>,
774
775 #[serde(skip_serializing_if = "Option::is_none")]
777 pub deployments: Option<Level>,
778
779 #[serde(skip_serializing_if = "Option::is_none")]
781 pub checks: Option<Level>,
782
783 #[serde(skip_serializing_if = "Option::is_none")]
785 pub statuses: Option<Level>,
786
787 #[serde(skip_serializing_if = "Option::is_none")]
789 pub packages: Option<Level>,
790
791 #[serde(skip_serializing_if = "Option::is_none")]
793 pub pages: Option<Level>,
794
795 #[serde(skip_serializing_if = "Option::is_none")]
797 pub id_token: Option<Level>,
798}
799
800#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
802#[serde(rename_all = "kebab-case")]
803pub enum Level {
804 Read,
805 Write,
806 #[default]
807 None,
808}
809
810#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
812#[serde(rename_all = "kebab-case")]
813#[setters(strip_option, into)]
814pub struct Strategy {
815 #[serde(skip_serializing_if = "Option::is_none")]
817 pub matrix: Option<Value>,
818
819 #[serde(skip_serializing_if = "Option::is_none")]
821 pub fail_fast: Option<bool>,
822
823 #[serde(skip_serializing_if = "Option::is_none")]
825 pub max_parallel: Option<u32>,
826}
827
828#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
830#[serde(rename_all = "kebab-case")]
831#[setters(strip_option, into)]
832pub struct Environment {
833 pub name: String,
835
836 #[serde(skip_serializing_if = "Option::is_none")]
838 pub url: Option<String>,
839}
840
841#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
843#[serde(rename_all = "kebab-case")]
844#[setters(strip_option, into)]
845pub struct Defaults {
846 #[serde(skip_serializing_if = "Option::is_none")]
848 pub run: Option<RunDefaults>,
849
850 #[serde(skip_serializing_if = "Option::is_none")]
852 pub retry: Option<RetryDefaults>,
853
854 #[serde(skip_serializing_if = "Option::is_none")]
856 pub concurrency: Option<Concurrency>,
857}
858
859#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
861#[serde(rename_all = "kebab-case")]
862#[setters(strip_option, into)]
863pub struct RunDefaults {
864 #[serde(skip_serializing_if = "Option::is_none")]
866 pub shell: Option<String>,
867
868 #[serde(skip_serializing_if = "Option::is_none")]
870 pub working_directory: Option<String>,
871}
872
873#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
875#[serde(rename_all = "kebab-case")]
876pub struct RetryDefaults {
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub max_attempts: Option<u32>,
880}
881
882#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
884pub struct Expression(String);
885
886impl Expression {
887 pub fn new<T: ToString>(expr: T) -> Self {
889 Self(expr.to_string())
890 }
891}
892
893#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
895#[serde(rename_all = "kebab-case")]
896#[setters(strip_option, into)]
897pub struct Secret {
898 pub required: bool,
900
901 #[serde(skip_serializing_if = "Option::is_none")]
903 pub description: Option<String>,
904}
905
906#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
908#[serde(rename_all = "kebab-case")]
909pub struct RetryStrategy {
910 #[serde(skip_serializing_if = "Option::is_none")]
912 pub max_attempts: Option<u32>,
913}
914
915#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
917#[serde(rename_all = "kebab-case")]
918#[setters(strip_option, into)]
919pub struct Artifacts {
920 #[serde(skip_serializing_if = "Option::is_none")]
922 pub upload: Option<Vec<Artifact>>,
923
924 #[serde(skip_serializing_if = "Option::is_none")]
926 pub download: Option<Vec<Artifact>>,
927}
928
929#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
931#[serde(rename_all = "kebab-case")]
932#[setters(strip_option, into)]
933pub struct Artifact {
934 pub name: String,
936
937 pub path: String,
939
940 #[serde(skip_serializing_if = "Option::is_none")]
942 pub retention_days: Option<u32>,
943}