github_actions_models/workflow/
event.rs

1//! Workflow events.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6use crate::common::EnvValue;
7
8/// "Bare" workflow event triggers.
9///
10/// These appear when a workflow is triggered with an event with no context,
11/// e.g.:
12///
13/// ```yaml
14/// on: push
15/// ```
16#[derive(Deserialize, PartialEq, Eq, Hash)]
17#[serde(rename_all = "snake_case")]
18pub enum BareEvent {
19    BranchProtectionRule,
20    CheckRun,
21    CheckSuite,
22    Create,
23    Delete,
24    Deployment,
25    DeploymentStatus,
26    Discussion,
27    DiscussionComment,
28    Fork,
29    Gollum,
30    IssueComment,
31    Issues,
32    Label,
33    MergeGroup,
34    Milestone,
35    PageBuild,
36    Project,
37    ProjectCard,
38    ProjectColumn,
39    Public,
40    PullRequest,
41    PullRequestComment,
42    PullRequestReview,
43    PullRequestReviewComment,
44    PullRequestTarget,
45    Push,
46    RegistryPackage,
47    Release,
48    RepositoryDispatch,
49    // NOTE: `schedule` is omitted, since it's never bare.
50    Status,
51    Watch,
52    WorkflowCall,
53    WorkflowDispatch,
54    WorkflowRun,
55}
56
57/// Workflow event triggers, with bodies.
58///
59/// Like [`BareEvent`], but with per-event properties.
60#[derive(Default, Deserialize, Serialize)]
61#[serde(default, rename_all = "snake_case")]
62pub struct Events {
63    pub branch_protection_rule: OptionalBody<GenericEvent>,
64    pub check_run: OptionalBody<GenericEvent>,
65    pub check_suite: OptionalBody<GenericEvent>,
66    // NOTE: `create` and `delete` are omitted, since they are always bare.
67    // NOTE: `deployment` and `deployment_status` are omitted, since they are always bare.
68    pub discussion: OptionalBody<GenericEvent>,
69    pub discussion_comment: OptionalBody<GenericEvent>,
70    // NOTE: `fork` and `gollum` are omitted, since they are always bare.
71    pub issue_comment: OptionalBody<GenericEvent>,
72    pub issues: OptionalBody<GenericEvent>,
73    pub label: OptionalBody<GenericEvent>,
74    pub merge_group: OptionalBody<GenericEvent>,
75    pub milestone: OptionalBody<GenericEvent>,
76    // NOTE: `page_build` is omitted, since it is always bare.
77    pub project: OptionalBody<GenericEvent>,
78    pub project_card: OptionalBody<GenericEvent>,
79    pub project_column: OptionalBody<GenericEvent>,
80    // NOTE: `public` is omitted, since it is always bare.
81    pub pull_request: OptionalBody<PullRequest>,
82    pub pull_request_comment: OptionalBody<GenericEvent>,
83    pub pull_request_review: OptionalBody<GenericEvent>,
84    pub pull_request_review_comment: OptionalBody<GenericEvent>,
85    // NOTE: `pull_request_target` appears to have the same trigger filters as `pull_request`.
86    pub pull_request_target: OptionalBody<PullRequest>,
87    pub push: OptionalBody<Push>,
88    pub registry_package: OptionalBody<GenericEvent>,
89    pub release: OptionalBody<GenericEvent>,
90    pub repository_dispatch: OptionalBody<GenericEvent>,
91    pub schedule: OptionalBody<Vec<Cron>>,
92    // NOTE: `status` is omitted, since it is always bare.
93    pub watch: OptionalBody<GenericEvent>,
94    pub workflow_call: OptionalBody<WorkflowCall>,
95    // TODO: Custom type.
96    pub workflow_dispatch: OptionalBody<WorkflowDispatch>,
97    pub workflow_run: OptionalBody<WorkflowRun>,
98}
99
100impl Events {
101    /// Count the number of present event triggers.
102    ///
103    /// **IMPORTANT**: This must be kept in sync with the number of fields in `Events`.
104    pub fn count(&self) -> u32 {
105        // This is a little goofy, but it's faster than reflecting over the struct
106        // or doing a serde round-trip.
107        let mut count = 0;
108
109        macro_rules! count_if_present {
110            ($($field:ident),*) => {
111                $(
112                    if !matches!(self.$field, OptionalBody::Missing) {
113                        count += 1;
114                    }
115                )*
116            };
117        }
118
119        count_if_present!(
120            branch_protection_rule,
121            check_run,
122            check_suite,
123            discussion,
124            discussion_comment,
125            issue_comment,
126            issues,
127            label,
128            merge_group,
129            milestone,
130            project,
131            project_card,
132            project_column,
133            pull_request,
134            pull_request_comment,
135            pull_request_review,
136            pull_request_review_comment,
137            pull_request_target,
138            push,
139            registry_package,
140            release,
141            repository_dispatch,
142            schedule,
143            watch,
144            workflow_call,
145            workflow_dispatch,
146            workflow_run
147        );
148
149        count
150    }
151}
152
153/// A generic container type for distinguishing between
154/// a missing key, an explicitly null key, and an explicit value `T`.
155///
156/// This is needed for modeling `on:` triggers, since GitHub distinguishes
157/// between the non-presence of an event (no trigger) and the presence
158/// of an empty event body (e.g. `pull_request:`), which means "trigger
159/// with the defaults for this event type."
160#[derive(Default, Serialize)]
161pub enum OptionalBody<T> {
162    Default,
163    #[default]
164    Missing,
165    Body(T),
166}
167
168impl<'de, T> Deserialize<'de> for OptionalBody<T>
169where
170    T: Deserialize<'de>,
171{
172    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
173    where
174        D: serde::Deserializer<'de>,
175    {
176        Option::deserialize(deserializer).map(Into::into)
177    }
178}
179
180impl<T> From<Option<T>> for OptionalBody<T> {
181    fn from(value: Option<T>) -> Self {
182        match value {
183            Some(v) => OptionalBody::Body(v),
184            None => OptionalBody::Default,
185        }
186    }
187}
188
189/// A generic event trigger body.
190#[derive(Deserialize, Serialize)]
191#[serde(rename_all = "kebab-case")]
192pub struct GenericEvent {
193    #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
194    pub types: Vec<String>,
195}
196
197/// The body of a `pull_request` event trigger.
198#[derive(Deserialize, Serialize)]
199#[serde(rename_all = "kebab-case")]
200pub struct PullRequest {
201    #[serde(default)]
202    pub types: Vec<String>,
203
204    #[serde(flatten)]
205    pub branch_filters: Option<BranchFilters>,
206
207    #[serde(flatten)]
208    pub path_filters: Option<PathFilters>,
209}
210
211/// The body of a `push` event trigger.
212#[derive(Deserialize, Serialize)]
213#[serde(rename_all = "kebab-case")]
214pub struct Push {
215    #[serde(flatten)]
216    pub branch_filters: Option<BranchFilters>,
217
218    #[serde(flatten)]
219    pub path_filters: Option<PathFilters>,
220
221    #[serde(flatten)]
222    pub tag_filters: Option<TagFilters>,
223}
224
225/// The body of a `cron` event trigger.
226#[derive(Deserialize, Serialize)]
227#[serde(rename_all = "kebab-case")]
228pub struct Cron {
229    pub cron: String,
230}
231
232/// The body of a `workflow_call` event trigger.
233#[derive(Deserialize, Serialize)]
234#[serde(rename_all = "kebab-case")]
235pub struct WorkflowCall {
236    #[serde(default)]
237    pub inputs: IndexMap<String, WorkflowCallInput>,
238    #[serde(default)]
239    pub outputs: IndexMap<String, WorkflowCallOutput>,
240    #[serde(default)]
241    pub secrets: IndexMap<String, Option<WorkflowCallSecret>>,
242}
243
244/// A single input in a `workflow_call` event trigger body.
245#[derive(Deserialize, Serialize)]
246#[serde(rename_all = "kebab-case")]
247pub struct WorkflowCallInput {
248    pub description: Option<String>,
249    // TODO: model `default`?
250    #[serde(default)]
251    pub required: bool,
252    pub r#type: String,
253}
254
255/// A single output in a `workflow_call` event trigger body.
256#[derive(Deserialize, Serialize)]
257#[serde(rename_all = "kebab-case")]
258pub struct WorkflowCallOutput {
259    pub description: Option<String>,
260    pub value: String,
261}
262
263/// A single secret in a `workflow_call` event trigger body.
264#[derive(Deserialize, Serialize)]
265#[serde(rename_all = "kebab-case")]
266pub struct WorkflowCallSecret {
267    pub description: Option<String>,
268    pub required: bool,
269}
270
271/// The body of a `workflow_dispatch` event trigger.
272#[derive(Deserialize, Serialize)]
273#[serde(rename_all = "kebab-case")]
274pub struct WorkflowDispatch {
275    #[serde(default)]
276    pub inputs: IndexMap<String, WorkflowDispatchInput>, // TODO: WorkflowDispatchInput
277}
278
279/// A single input in a `workflow_dispatch` event trigger body.
280#[derive(Deserialize, Serialize)]
281#[serde(rename_all = "kebab-case")]
282pub struct WorkflowDispatchInput {
283    pub description: Option<String>,
284    // TODO: model `default`?
285    #[serde(default)]
286    pub required: bool,
287    // TODO: Model as boolean, choice, number, environment, string; default is string.
288    pub r#type: Option<String>,
289    // Only present when `type` is `choice`.
290    #[serde(default)]
291    pub options: Vec<EnvValue>,
292}
293
294/// The body of a `workflow_run` event trigger.
295#[derive(Deserialize, Serialize)]
296#[serde(rename_all = "kebab-case")]
297pub struct WorkflowRun {
298    pub workflows: Vec<String>,
299    #[serde(default)]
300    pub types: Vec<String>,
301    #[serde(flatten)]
302    pub branch_filters: Option<BranchFilters>,
303}
304
305/// Branch filtering variants for event trigger bodies.
306#[derive(Deserialize, Serialize)]
307#[serde(rename_all = "kebab-case")]
308pub enum BranchFilters {
309    Branches(Vec<String>),
310    BranchesIgnore(Vec<String>),
311}
312
313/// Tag filtering variants for event trigger bodies.
314#[derive(Deserialize, Serialize)]
315#[serde(rename_all = "kebab-case")]
316pub enum TagFilters {
317    Tags(Vec<String>),
318    TagsIgnore(Vec<String>),
319}
320
321/// Path filtering variants for event trigger bodies.
322#[derive(Deserialize, Serialize)]
323#[serde(rename_all = "kebab-case")]
324pub enum PathFilters {
325    Paths(Vec<String>),
326    PathsIgnore(Vec<String>),
327}
328
329#[cfg(test)]
330mod tests {
331    #[test]
332    fn test_events_count() {
333        let events = "
334push:
335pull_request:
336workflow_dispatch:
337issue_comment:";
338
339        let events = serde_yaml::from_str::<super::Events>(events).unwrap();
340        assert_eq!(events.count(), 4);
341    }
342}