Skip to main content

github_actions_models/workflow/
mod.rs

1//! Data models for GitHub Actions workflow definitions.
2//!
3//! Resources:
4//! * [Workflow syntax for GitHub Actions]
5//! * [JSON Schema definition for workflows]
6//!
7//! [Workflow Syntax for GitHub Actions]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions>
8//! [JSON Schema definition for workflows]: https://json.schemastore.org/github-workflow.json
9
10use indexmap::IndexMap;
11use serde::Deserialize;
12
13use crate::common::{
14    Env, Permissions,
15    expr::{BoE, LoE},
16};
17
18pub mod event;
19pub mod job;
20
21/// A single GitHub Actions workflow.
22#[derive(Deserialize, Debug)]
23#[serde(rename_all = "kebab-case")]
24pub struct Workflow {
25    pub name: Option<String>,
26    pub run_name: Option<String>,
27    pub on: Trigger,
28    #[serde(default)]
29    pub permissions: Permissions,
30    #[serde(default)]
31    pub env: LoE<Env>,
32    pub defaults: Option<Defaults>,
33    pub concurrency: Option<Concurrency>,
34    pub jobs: IndexMap<String, Job>,
35}
36
37/// The triggering condition or conditions for a workflow.
38///
39/// Workflow triggers take three forms:
40///
41/// 1. A single webhook event name:
42///
43///     ```yaml
44///     on: push
45///     ```
46/// 2. A list of webhook event names:
47///
48///     ```yaml
49///     on: [push, fork]
50///     ```
51///
52/// 3. A mapping of event names with (optional) configurations:
53///
54///     ```yaml
55///     on:
56///       push:
57///         branches: [main]
58///       pull_request:
59///     ```
60#[derive(Deserialize, Debug)]
61#[serde(rename_all = "snake_case", untagged)]
62pub enum Trigger {
63    // NOTE: `Events` is before `BareEvent` because serde-yaml appears to capture
64    // `pull_request:` (an event with a missing body) as equivalent to `on: pull_request`
65    // (a bare event).
66    Events(Box<event::Events>),
67    BareEvent(event::BareEvent),
68    BareEvents(Vec<event::BareEvent>),
69}
70
71#[derive(Deserialize, Debug)]
72#[serde(rename_all = "kebab-case")]
73pub struct Defaults {
74    pub run: Option<RunDefaults>,
75}
76
77#[derive(Deserialize, Debug)]
78#[serde(rename_all = "kebab-case")]
79pub struct RunDefaults {
80    pub shell: Option<LoE<String>>,
81    pub working_directory: Option<String>,
82}
83
84#[derive(Deserialize, Debug)]
85#[serde(rename_all_fields = "kebab-case", untagged)]
86pub enum Concurrency {
87    Bare(String),
88    Rich {
89        group: String,
90        #[serde(default)]
91        cancel_in_progress: BoE,
92    },
93}
94
95#[derive(Deserialize, Debug)]
96#[serde(rename_all = "kebab-case", untagged)]
97pub enum Job {
98    NormalJob(Box<job::NormalJob>),
99    ReusableWorkflowCallJob(Box<job::ReusableWorkflowCallJob>),
100}
101
102impl Job {
103    /// Returns the optional `name` field common to both reusable and normal
104    /// job definitions.
105    pub fn name(&self) -> Option<&str> {
106        match self {
107            Self::NormalJob(job) => job.name.as_deref(),
108            Self::ReusableWorkflowCallJob(job) => job.name.as_deref(),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use crate::{
116        common::expr::BoE,
117        workflow::event::{OptionalBody, WorkflowCall, WorkflowDispatch},
118    };
119
120    use super::{Concurrency, Trigger};
121
122    #[test]
123    fn test_concurrency() {
124        let bare = "foo";
125        let concurrency: Concurrency = yaml_serde::from_str(bare).unwrap();
126        assert!(matches!(concurrency, Concurrency::Bare(_)));
127
128        let rich = "group: foo\ncancel-in-progress: true";
129        let concurrency: Concurrency = yaml_serde::from_str(rich).unwrap();
130        assert!(matches!(
131            concurrency,
132            Concurrency::Rich {
133                group: _,
134                cancel_in_progress: BoE::Literal(true)
135            }
136        ));
137    }
138
139    #[test]
140    fn test_workflow_triggers() {
141        let on = "
142  issues:
143  workflow_dispatch:
144    inputs:
145      foo:
146        type: string
147  workflow_call:
148    inputs:
149      bar:
150        type: string
151  pull_request_target:
152        ";
153
154        let trigger: Trigger = yaml_serde::from_str(on).unwrap();
155        let Trigger::Events(events) = trigger else {
156            panic!("wrong trigger type");
157        };
158
159        assert!(matches!(events.issues, OptionalBody::Default));
160        assert!(matches!(
161            events.workflow_dispatch,
162            OptionalBody::Body(WorkflowDispatch { .. })
163        ));
164        assert!(matches!(
165            events.workflow_call,
166            OptionalBody::Body(WorkflowCall { .. })
167        ));
168        assert!(matches!(events.pull_request_target, OptionalBody::Default));
169    }
170}