1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Data models for GitHub Actions action definitions.
//!
//! Resources:
//! * [Metadata syntax for GitHub Actions]
//! * [JSON Schema definition for GitHub Actions]
//!
//! [Metadata syntax for GitHub Actions]: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
//! [JSON Schema definition for GitHub Actions]: https://json.schemastore.org/github-action.json

use std::collections::HashMap;

use serde::Deserialize;

use crate::common::{BoE, Env};

/// A GitHub Actions action definition.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Action {
    pub name: String,
    pub author: Option<String>,
    pub description: Option<String>,
    #[serde(default)]
    pub inputs: HashMap<String, Input>,
    #[serde(default)]
    pub outputs: HashMap<String, Output>,
    pub runs: Runs,
}

/// An action input.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Input {
    pub description: String,
    pub required: Option<bool>,
    pub default: Option<String>,
}

/// An action output.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Output {
    pub description: String,
    // NOTE: not optional for composite actions, but this is not worth modeling.
    pub value: Option<String>,
}

/// An action `runs` definition.
///
/// A `runs` definition can be either a JavaScript action, a "composite" action
/// (made up of several constituent actions), or a Docker action.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum Runs {
    JavaScript(JavaScript),
    Composite(Composite),
    Docker(Docker),
}

/// A `runs` definition for a JavaScript action.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct JavaScript {
    /// The Node runtime to use for this action. This is one of:
    ///
    /// `"node12" | "node16" | "node20"`
    pub using: String,

    /// The action's entrypoint, as a JavaScript file.
    pub main: String,

    /// An optional script to run, before [`JavaScript::main`].
    pub pre: Option<String>,

    /// An optional expression that triggers [`JavaScript::pre`] if it evaluates to `true`.
    ///
    /// If not present, defaults to `always()`
    pub pre_if: Option<String>,

    /// An optional script to run, after [`JavaScript::main`].
    pub post: Option<String>,

    /// An optional expression that triggers [`JavaScript::post`] if it evaluates to `true`.
    ///
    /// If not present, defaults to `always()`
    pub post_if: Option<String>,
}

/// A `runs` definition for a composite action.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Composite {
    /// Invariant: `"composite"`
    pub using: String,
    /// The individual steps that make up this composite action.
    pub steps: Vec<Step>,
}

/// An individual composite action step.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum Step {
    RunShell(RunShell),
    UseAction(UseAction),
}

/// A step that runs a command in a shell.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct RunShell {
    /// The command to run.
    pub run: String,

    /// The shell to run in.
    pub shell: String,

    /// An optional name for this step.
    pub name: Option<String>,

    /// An optional ID for this step.
    pub id: Option<String>,

    /// An optional expression that prevents this step from running unless it evaluates to `true`.
    pub r#if: Option<String>,

    /// An optional environment mapping for this step.
    #[serde(default)]
    pub env: Env,

    /// A an optional boolean or expression that, if `true`, prevents the job from failing when
    /// this step fails.
    #[serde(default)]
    pub continue_on_error: BoE,

    /// An optional working directory to run [`RunShell::run`] from.
    pub working_directory: Option<String>,
}

/// A step that uses another GitHub Action.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct UseAction {
    /// The GitHub Action being used.
    pub uses: String,

    /// Any inputs to the action being used.
    #[serde(default)]
    pub with: HashMap<String, String>,

    /// An optional expression that prevents this step from running unless it evaluates to `true`.
    pub r#if: Option<String>,
}

/// A `runs` definition for a Docker action.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Docker {
    /// Invariant: `"docker"`
    pub using: String,

    /// The Docker image to use.
    pub image: String,

    /// An optional environment mapping for this step.
    #[serde(default)]
    pub env: Env,

    /// An optional Docker entrypoint, potentially overriding the image's
    /// default entrypoint.
    pub entrypoint: Option<String>,

    /// An optional "pre" entrypoint to run, before [`Docker::entrypoint`].
    pub pre_entrypoint: Option<String>,

    /// An optional expression that triggers [`Docker::pre_entrypoint`] if it evaluates to `true`.
    ///
    /// If not present, defaults to `always()`
    pub pre_if: Option<String>,

    /// An optional "post" entrypoint to run, after [`Docker::entrypoint`] or the default
    /// entrypoint.
    pub post_entrypoint: Option<String>,

    /// An optional expression that triggers [`Docker::post_entrypoint`] if it evaluates to `true`.
    ///
    /// If not present, defaults to `always()`
    pub post_if: Option<String>,
}