gh_workflow/
workflow.rs

1//!
2//! The serde representation of Github Actions Workflow.
3
4use 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    /// Gets a reference to a job by its key.
25    ///
26    /// # Arguments
27    ///
28    /// * `key` - The key of the job to retrieve
29    ///
30    /// # Returns
31    ///
32    /// Returns `Some(&Job)` if the job exists, `None` otherwise.
33    pub fn get(&self, key: &str) -> Option<&Job> {
34        self.0.get(key)
35    }
36}
37
38/// Represents the configuration for a GitHub workflow.
39///
40/// A workflow is a configurable automated process made up of one or more jobs.
41/// This struct defines the properties that can be set in a workflow YAML file
42/// for GitHub Actions, including the name, environment variables, permissions,
43/// jobs, concurrency settings, and more.
44#[derive(Debug, Default, Setters, Serialize, Deserialize, Clone)]
45#[serde(rename_all = "kebab-case")]
46#[setters(strip_option, into)]
47pub struct Workflow {
48    /// The name of the workflow. GitHub displays the names of your workflows
49    /// under your repository's "Actions" tab.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub name: Option<String>,
52
53    /// Environment variables that can be used in the workflow.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub env: Option<Env>,
56
57    /// The name for workflow runs generated from the workflow.
58    /// GitHub displays the workflow run name in the list of workflow runs.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub run_name: Option<String>,
61
62    /// The event that triggers the workflow. This can include events like
63    /// `push`, `pull_request`, etc.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub on: Option<Event>,
66
67    /// Permissions granted to the `GITHUB_TOKEN` for the workflow.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub permissions: Option<Permissions>,
70
71    /// The jobs that are defined in the workflow.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub jobs: Option<Jobs>,
74
75    /// Concurrency settings for the workflow, allowing control over
76    /// how jobs are executed.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub concurrency: Option<Concurrency>,
79
80    /// Default settings for jobs in the workflow.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub defaults: Option<Defaults>,
83
84    /// Secrets that can be used in the workflow, such as tokens or passwords.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub secrets: Option<IndexMap<String, Secret>>,
87
88    /// The maximum number of minutes a job can run before it is canceled.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub timeout_minutes: Option<u32>,
91}
92
93/// Represents an action that can be triggered by an event in the workflow.
94#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
95#[serde(rename_all = "kebab-case")]
96pub struct EventAction {
97    /// A list of branches that trigger the action.
98    #[serde(skip_serializing_if = "Vec::is_empty")]
99    branches: Vec<String>,
100
101    /// A list of branches that are ignored for the action.
102    #[serde(skip_serializing_if = "Vec::is_empty")]
103    branches_ignore: Vec<String>,
104}
105
106impl Workflow {
107    /// Creates a new `Workflow` with the specified name.
108    pub fn new<T: ToString>(name: T) -> Self {
109        Self { name: Some(name.to_string()), ..Default::default() }
110    }
111
112    /// Converts the `Workflow` to a YAML string representation.
113    pub fn to_string(&self) -> Result<String> {
114        Ok(serde_yaml::to_string(self)?)
115    }
116
117    /// Adds a job to the workflow when a condition is met.
118    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    /// Adds a job to the workflow with the specified ID and job configuration.
127    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    /// Parses a YAML string into a `Workflow`.
138    pub fn parse(yml: &str) -> Result<Self> {
139        Ok(serde_yaml::from_str(yml)?)
140    }
141
142    /// Generates the workflow using the `Generate` struct.
143    pub fn generate(self) -> Result<()> {
144        Generate::new(self).generate()
145    }
146
147    /// Adds an event to the workflow.
148    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    /// Adds an event to the workflow when a condition is met.
159    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    /// Adds an environment variable to the workflow.
168    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    /// Adds an environment variable to the workflow when a condition is met.
177    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    /// Performs a reverse lookup to get the ID of a job.
186    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/// Represents the type of activity in the workflow.
197#[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/// Represents the environment in which a job runs.
206#[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    /// Converts a value into a `RunsOn` instance.
215    fn from(value: T) -> Self {
216        RunsOn(value.into())
217    }
218}
219
220/// Represents a job in the workflow.
221#[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    /// Creates a new `Job` with the specified name and default settings.
272    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    /// Adds a step to the job when a condition is met.
281    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    /// Adds a step to the job.
290    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    /// Adds an environment variable to the job.
300    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    /// Adds an environment variable to the job when a condition is met.
309    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    /// Add multiple steps to the job at once.
318    ///
319    /// This is a convenience method that takes a vector of steps and adds them
320    /// all to the job in order.
321    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    /// Adds a dependency to the job when a condition is met.
337    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/// Represents a step in the workflow.
347#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
348#[serde(transparent)]
349pub struct Step<A> {
350    /// The value of the step.
351    value: StepValue,
352    #[serde(skip)]
353    marker: std::marker::PhantomData<A>,
354}
355
356impl From<Step<Run>> for StepValue {
357    /// Converts a `Step<Run>` into a `StepValue`.
358    fn from(step: Step<Run>) -> Self {
359        step.value
360    }
361}
362
363impl From<Step<Use>> for StepValue {
364    /// Converts a `Step<Use>` into a `StepValue`.
365    fn from(step: Step<Use>) -> Self {
366        step.value
367    }
368}
369
370/// Represents a step that uses an action.
371#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
372pub struct Use;
373
374/// Represents a step that runs a command.
375#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
376pub struct Run;
377
378/// A trait to convert `Step<Run>` and `Step<Use>` to `StepValue`.
379pub trait StepType: Sized + private::Sealed {
380    /// Converts a step to its value representation.
381    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    /// Converts a `Step<Run>` to `StepValue`.
389    fn to_value(s: Step<Self>) -> StepValue {
390        s.into()
391    }
392}
393
394impl StepType for Use {
395    /// Converts a `Step<Use>` to `StepValue`.
396    fn to_value(s: Step<Self>) -> StepValue {
397        s.into()
398    }
399}
400
401/// Represents environment variables in the workflow.
402#[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    /// Converts an `IndexMap` into an `Env`.
408    fn from(value: IndexMap<String, Value>) -> Self {
409        Env(value)
410    }
411}
412
413impl Env {
414    /// Sets the `GITHUB_TOKEN` environment variable.
415    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    /// Creates a new `Env` with a specified key-value pair.
425    pub fn new<K: ToString, V: Into<Value>>(key: K, value: V) -> Self {
426        Env::default().add(key, value)
427    }
428
429    /// Adds an environment variable to the `Env`.
430    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/// Represents input parameters for a step.
437#[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    /// Converts an `IndexMap` into an `Input`.
443    fn from(value: IndexMap<String, Value>) -> Self {
444        Input(value)
445    }
446}
447
448impl Merge for Input {
449    /// Merges another `Input` into this one.
450    fn merge(&mut self, other: Self) {
451        self.0.extend(other.0);
452    }
453}
454
455impl Input {
456    /// Adds a new input parameter to the `Input`.
457    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    /// Checks if the `Input` is empty.
463    pub fn is_empty(&self) -> bool {
464        self.0.is_empty()
465    }
466}
467
468/// Represents a step value in the workflow.
469#[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    /// The ID of the step.
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub id: Option<String>,
482
483    /// The name of the step.
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub name: Option<String>,
486
487    /// The condition under which the step runs.
488    #[serde(skip_serializing_if = "Option::is_none", rename = "if")]
489    pub if_condition: Option<Expression>,
490
491    /// The action to use in the step.
492    #[serde(skip_serializing_if = "Option::is_none")]
493    #[setters(skip)]
494    pub uses: Option<String>,
495
496    /// Input parameters for the step.
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub with: Option<Input>,
499
500    /// The command to run in the step.
501    #[serde(skip_serializing_if = "Option::is_none")]
502    #[setters(skip)]
503    pub run: Option<String>,
504
505    /// Environment variables for the step.
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub env: Option<Env>,
508
509    /// The timeout for the step in minutes.
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub timeout_minutes: Option<u32>,
512
513    /// Whether to continue on error.
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub continue_on_error: Option<bool>,
516
517    /// The working directory for the step.
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub working_directory: Option<String>,
520
521    /// The retry strategy for the step.
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub retry: Option<RetryStrategy>,
524
525    /// Artifacts produced by the step.
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub artifacts: Option<Artifacts>,
528}
529
530impl StepValue {
531    /// Creates a new `StepValue` that runs the provided shell command.
532    pub fn run<T: ToString>(cmd: T) -> Self {
533        StepValue { run: Some(cmd.to_string()), ..Default::default() }
534    }
535
536    /// Creates a new `StepValue` that uses an action.
537    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
554/// Represents a step in the workflow.
555impl<T> Step<T> {
556    /// Adds an environment variable to the step.
557    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
566/// Represents a step that runs a command.
567impl Step<Run> {
568    /// Creates a new `Step<Run>` that runs the provided shell command.
569    pub fn run<T: ToString>(cmd: T) -> Self {
570        Step { value: StepValue::run(cmd), marker: Default::default() }
571    }
572}
573
574/// Represents a step that uses an action.
575impl Step<Use> {
576    /// Creates a new `Step<Use>` that uses an action.
577    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    /// Creates a step pointing to the default GitHub's Checkout Action.
589    pub fn checkout() -> Step<Use> {
590        Step::uses("actions", "checkout", "v4").name("Checkout Code")
591    }
592
593    /// Adds a new input to the step.
594    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    /// Adds a new input to the step when a condition is met.
607    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
616/// Represents a key-value pair for inputs.
617impl<S1: ToString, S2: ToString> From<(S1, S2)> for Input {
618    /// Converts a tuple into an `Input`.
619    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
626/// Represents environment variables as key-value pairs.
627impl<S1: Display, S2: Display> From<(S1, S2)> for Env {
628    /// Converts a tuple into an `Env`.
629    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/// Represents the runner environment for jobs.
637#[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/// Represents a container configuration for jobs.
648#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
649#[serde(rename_all = "kebab-case")]
650#[setters(strip_option, into)]
651pub struct Container {
652    /// The image to use for the container.
653    pub image: String,
654
655    /// Credentials for accessing the container.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub credentials: Option<Credentials>,
658
659    /// Environment variables for the container.
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub env: Option<Env>,
662
663    /// Ports to expose from the container.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub ports: Option<Vec<Port>>,
666
667    /// Volumes to mount in the container.
668    #[serde(skip_serializing_if = "Option::is_none")]
669    pub volumes: Option<Vec<Volume>>,
670
671    /// Additional options for the container.
672    #[serde(skip_serializing_if = "Option::is_none")]
673    pub options: Option<String>,
674
675    /// Hostname for the container.
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub hostname: Option<String>,
678}
679
680/// Represents credentials for accessing a container.
681#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
682#[serde(rename_all = "kebab-case")]
683#[setters(strip_option, into)]
684pub struct Credentials {
685    /// The username for authentication.
686    pub username: String,
687
688    /// The password for authentication.
689    pub password: String,
690}
691
692/// Represents a network port.
693#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
694#[serde(rename_all = "kebab-case")]
695pub enum Port {
696    /// A port specified by its number.
697    Number(u16),
698
699    /// A port specified by its name.
700    Name(String),
701}
702
703/// Represents a volume configuration for containers.
704#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
705#[serde(rename_all = "kebab-case")]
706#[setters(strip_option, into)]
707pub struct Volume {
708    /// The source path of the volume.
709    pub source: String,
710
711    /// The destination path of the volume.
712    pub destination: String,
713}
714
715impl Volume {
716    /// Creates a new `Volume` from a string representation.
717    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/// Represents concurrency settings for workflows.
731#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
732#[serde(rename_all = "kebab-case")]
733#[setters(strip_option, into)]
734pub struct Concurrency {
735    /// The group name for concurrency.
736    pub group: String,
737
738    /// Whether to cancel in-progress jobs.
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub cancel_in_progress: Option<bool>,
741
742    /// The limit on concurrent jobs.
743    #[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/// Represents permissions for the `GITHUB_TOKEN`.
755#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
756#[serde(rename_all = "kebab-case")]
757#[setters(strip_option, into)]
758pub struct Permissions {
759    /// Permissions for actions.
760    #[serde(skip_serializing_if = "Option::is_none")]
761    pub actions: Option<Level>,
762
763    /// Permissions for repository contents.
764    #[serde(skip_serializing_if = "Option::is_none")]
765    pub contents: Option<Level>,
766
767    /// Permissions for issues.
768    #[serde(skip_serializing_if = "Option::is_none")]
769    pub issues: Option<Level>,
770
771    /// Permissions for pull requests.
772    #[serde(skip_serializing_if = "Option::is_none")]
773    pub pull_requests: Option<Level>,
774
775    /// Permissions for deployments.
776    #[serde(skip_serializing_if = "Option::is_none")]
777    pub deployments: Option<Level>,
778
779    /// Permissions for checks.
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub checks: Option<Level>,
782
783    /// Permissions for statuses.
784    #[serde(skip_serializing_if = "Option::is_none")]
785    pub statuses: Option<Level>,
786
787    /// Permissions for packages.
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub packages: Option<Level>,
790
791    /// Permissions for pages.
792    #[serde(skip_serializing_if = "Option::is_none")]
793    pub pages: Option<Level>,
794
795    /// Permissions for ID tokens.
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub id_token: Option<Level>,
798}
799
800/// Represents the level of permissions.
801#[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/// Represents the strategy for running jobs.
811#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
812#[serde(rename_all = "kebab-case")]
813#[setters(strip_option, into)]
814pub struct Strategy {
815    /// The matrix for job execution.
816    #[serde(skip_serializing_if = "Option::is_none")]
817    pub matrix: Option<Value>,
818
819    /// Whether to fail fast on errors.
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub fail_fast: Option<bool>,
822
823    /// The maximum number of jobs to run in parallel.
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub max_parallel: Option<u32>,
826}
827
828/// Represents an environment for jobs.
829#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
830#[serde(rename_all = "kebab-case")]
831#[setters(strip_option, into)]
832pub struct Environment {
833    /// The name of the environment.
834    pub name: String,
835
836    /// The URL associated with the environment.
837    #[serde(skip_serializing_if = "Option::is_none")]
838    pub url: Option<String>,
839}
840
841/// Represents default settings for jobs.
842#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
843#[serde(rename_all = "kebab-case")]
844#[setters(strip_option, into)]
845pub struct Defaults {
846    /// Default settings for running jobs.
847    #[serde(skip_serializing_if = "Option::is_none")]
848    pub run: Option<RunDefaults>,
849
850    /// Default retry settings for jobs.
851    #[serde(skip_serializing_if = "Option::is_none")]
852    pub retry: Option<RetryDefaults>,
853
854    /// Default concurrency settings for jobs.
855    #[serde(skip_serializing_if = "Option::is_none")]
856    pub concurrency: Option<Concurrency>,
857}
858
859/// Represents default settings for running commands.
860#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
861#[serde(rename_all = "kebab-case")]
862#[setters(strip_option, into)]
863pub struct RunDefaults {
864    /// The shell to use for running commands.
865    #[serde(skip_serializing_if = "Option::is_none")]
866    pub shell: Option<String>,
867
868    /// The working directory for running commands.
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub working_directory: Option<String>,
871}
872
873/// Represents default settings for retries.
874#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
875#[serde(rename_all = "kebab-case")]
876pub struct RetryDefaults {
877    /// The maximum number of retry attempts.
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub max_attempts: Option<u32>,
880}
881
882/// Represents an expression used in conditions.
883#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
884pub struct Expression(String);
885
886impl Expression {
887    /// Creates a new `Expression` from a string.
888    pub fn new<T: ToString>(expr: T) -> Self {
889        Self(expr.to_string())
890    }
891}
892
893/// Represents a secret required for the workflow.
894#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
895#[serde(rename_all = "kebab-case")]
896#[setters(strip_option, into)]
897pub struct Secret {
898    /// Indicates if the secret is required.
899    pub required: bool,
900
901    /// A description of the secret.
902    #[serde(skip_serializing_if = "Option::is_none")]
903    pub description: Option<String>,
904}
905
906/// Represents a strategy for retrying jobs.
907#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
908#[serde(rename_all = "kebab-case")]
909pub struct RetryStrategy {
910    /// The maximum number of retry attempts.
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub max_attempts: Option<u32>,
913}
914
915/// Represents artifacts produced by jobs.
916#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
917#[serde(rename_all = "kebab-case")]
918#[setters(strip_option, into)]
919pub struct Artifacts {
920    /// Artifacts to upload after the job.
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub upload: Option<Vec<Artifact>>,
923
924    /// Artifacts to download before the job.
925    #[serde(skip_serializing_if = "Option::is_none")]
926    pub download: Option<Vec<Artifact>>,
927}
928
929/// Represents an artifact produced by a job.
930#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
931#[serde(rename_all = "kebab-case")]
932#[setters(strip_option, into)]
933pub struct Artifact {
934    /// The name of the artifact.
935    pub name: String,
936
937    /// The path to the artifact.
938    pub path: String,
939
940    /// The number of days to retain the artifact.
941    #[serde(skip_serializing_if = "Option::is_none")]
942    pub retention_days: Option<u32>,
943}